@webex/plugin-meetings 3.11.0 → 3.12.0-next.2
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/dist/aiEnableRequest/index.js +184 -0
- package/dist/aiEnableRequest/index.js.map +1 -0
- package/dist/aiEnableRequest/utils.js +36 -0
- package/dist/aiEnableRequest/utils.js.map +1 -0
- package/dist/annotation/index.js +14 -5
- package/dist/annotation/index.js.map +1 -1
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/config.js +7 -2
- package/dist/config.js.map +1 -1
- package/dist/constants.js +28 -6
- package/dist/constants.js.map +1 -1
- package/dist/hashTree/constants.js +3 -1
- package/dist/hashTree/constants.js.map +1 -1
- package/dist/hashTree/hashTree.js +18 -0
- package/dist/hashTree/hashTree.js.map +1 -1
- package/dist/hashTree/hashTreeParser.js +850 -410
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/hashTree/types.js +4 -2
- package/dist/hashTree/types.js.map +1 -1
- package/dist/hashTree/utils.js +10 -0
- package/dist/hashTree/utils.js.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/interceptors/constant.js +12 -0
- package/dist/interceptors/constant.js.map +1 -0
- package/dist/interceptors/dataChannelAuthToken.js +290 -0
- package/dist/interceptors/dataChannelAuthToken.js.map +1 -0
- package/dist/interceptors/index.js +7 -0
- package/dist/interceptors/index.js.map +1 -1
- package/dist/interceptors/utils.js +27 -0
- package/dist/interceptors/utils.js.map +1 -0
- package/dist/interpretation/index.js +2 -2
- package/dist/interpretation/index.js.map +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/controlsUtils.js +5 -3
- package/dist/locus-info/controlsUtils.js.map +1 -1
- package/dist/locus-info/index.js +522 -131
- package/dist/locus-info/index.js.map +1 -1
- package/dist/locus-info/selfUtils.js +1 -0
- package/dist/locus-info/selfUtils.js.map +1 -1
- package/dist/locus-info/types.js.map +1 -1
- package/dist/media/MediaConnectionAwaiter.js +57 -1
- package/dist/media/MediaConnectionAwaiter.js.map +1 -1
- package/dist/media/properties.js +4 -2
- package/dist/media/properties.js.map +1 -1
- package/dist/meeting/in-meeting-actions.js +7 -1
- package/dist/meeting/in-meeting-actions.js.map +1 -1
- package/dist/meeting/index.js +1173 -877
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/request.js +50 -0
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/request.type.js.map +1 -1
- package/dist/meeting/util.js +133 -3
- package/dist/meeting/util.js.map +1 -1
- package/dist/meetings/index.js +117 -48
- package/dist/meetings/index.js.map +1 -1
- package/dist/member/index.js +10 -0
- package/dist/member/index.js.map +1 -1
- package/dist/member/util.js +10 -0
- package/dist/member/util.js.map +1 -1
- package/dist/metrics/constants.js +2 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/multistream/mediaRequestManager.js +9 -60
- package/dist/multistream/mediaRequestManager.js.map +1 -1
- package/dist/multistream/remoteMediaManager.js +11 -0
- package/dist/multistream/remoteMediaManager.js.map +1 -1
- package/dist/reachability/index.js +18 -10
- package/dist/reachability/index.js.map +1 -1
- package/dist/reactions/reactions.type.js.map +1 -1
- package/dist/reconnection-manager/index.js +0 -1
- package/dist/reconnection-manager/index.js.map +1 -1
- package/dist/types/aiEnableRequest/index.d.ts +5 -0
- package/dist/types/aiEnableRequest/utils.d.ts +2 -0
- package/dist/types/config.d.ts +4 -0
- package/dist/types/constants.d.ts +23 -1
- package/dist/types/hashTree/constants.d.ts +1 -0
- package/dist/types/hashTree/hashTree.d.ts +7 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +122 -14
- package/dist/types/hashTree/types.d.ts +3 -0
- package/dist/types/hashTree/utils.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/interceptors/constant.d.ts +5 -0
- package/dist/types/interceptors/dataChannelAuthToken.d.ts +43 -0
- package/dist/types/interceptors/index.d.ts +2 -1
- package/dist/types/interceptors/utils.d.ts +1 -0
- package/dist/types/locus-info/index.d.ts +60 -8
- package/dist/types/locus-info/types.d.ts +7 -0
- package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
- package/dist/types/media/properties.d.ts +2 -1
- package/dist/types/meeting/in-meeting-actions.d.ts +6 -0
- package/dist/types/meeting/index.d.ts +61 -7
- package/dist/types/meeting/request.d.ts +16 -1
- package/dist/types/meeting/request.type.d.ts +5 -0
- package/dist/types/meeting/util.d.ts +31 -0
- package/dist/types/meetings/index.d.ts +4 -2
- package/dist/types/member/index.d.ts +1 -0
- package/dist/types/member/util.d.ts +5 -0
- package/dist/types/metrics/constants.d.ts +1 -0
- package/dist/types/multistream/mediaRequestManager.d.ts +0 -23
- package/dist/types/reactions/reactions.type.d.ts +1 -0
- package/dist/types/webinar/utils.d.ts +6 -0
- package/dist/webinar/index.js +291 -91
- package/dist/webinar/index.js.map +1 -1
- package/dist/webinar/utils.js +25 -0
- package/dist/webinar/utils.js.map +1 -0
- package/package.json +24 -23
- package/src/aiEnableRequest/README.md +84 -0
- package/src/aiEnableRequest/index.ts +170 -0
- package/src/aiEnableRequest/utils.ts +25 -0
- package/src/annotation/index.ts +27 -7
- package/src/config.ts +4 -0
- package/src/constants.ts +29 -1
- package/src/hashTree/constants.ts +1 -0
- package/src/hashTree/hashTree.ts +17 -0
- package/src/hashTree/hashTreeParser.ts +745 -252
- package/src/hashTree/types.ts +4 -0
- package/src/hashTree/utils.ts +9 -0
- package/src/index.ts +8 -1
- package/src/interceptors/constant.ts +6 -0
- package/src/interceptors/dataChannelAuthToken.ts +170 -0
- package/src/interceptors/index.ts +2 -1
- package/src/interceptors/utils.ts +16 -0
- package/src/interpretation/index.ts +2 -2
- package/src/locus-info/controlsUtils.ts +11 -0
- package/src/locus-info/index.ts +579 -113
- package/src/locus-info/selfUtils.ts +1 -0
- package/src/locus-info/types.ts +8 -0
- package/src/media/MediaConnectionAwaiter.ts +41 -1
- package/src/media/properties.ts +3 -1
- package/src/meeting/in-meeting-actions.ts +12 -0
- package/src/meeting/index.ts +291 -76
- package/src/meeting/request.ts +42 -0
- package/src/meeting/request.type.ts +6 -0
- package/src/meeting/util.ts +160 -2
- package/src/meetings/index.ts +157 -44
- package/src/member/index.ts +10 -0
- package/src/member/util.ts +12 -0
- package/src/metrics/constants.ts +1 -0
- package/src/multistream/mediaRequestManager.ts +4 -54
- package/src/multistream/remoteMediaManager.ts +13 -0
- package/src/reachability/index.ts +9 -0
- package/src/reactions/reactions.type.ts +1 -0
- package/src/reconnection-manager/index.ts +0 -1
- package/src/webinar/index.ts +191 -6
- package/src/webinar/utils.ts +16 -0
- package/test/unit/spec/aiEnableRequest/index.ts +981 -0
- package/test/unit/spec/aiEnableRequest/utils.ts +130 -0
- package/test/unit/spec/annotation/index.ts +69 -7
- package/test/unit/spec/hashTree/hashTree.ts +66 -0
- package/test/unit/spec/hashTree/hashTreeParser.ts +2225 -189
- package/test/unit/spec/interceptors/dataChannelAuthToken.ts +210 -0
- package/test/unit/spec/interceptors/utils.ts +75 -0
- package/test/unit/spec/locus-info/controlsUtils.js +29 -0
- package/test/unit/spec/locus-info/index.js +1134 -55
- package/test/unit/spec/media/MediaConnectionAwaiter.ts +41 -1
- package/test/unit/spec/media/properties.ts +12 -3
- package/test/unit/spec/meeting/in-meeting-actions.ts +8 -2
- package/test/unit/spec/meeting/index.js +829 -115
- package/test/unit/spec/meeting/request.js +70 -0
- package/test/unit/spec/meeting/utils.js +438 -26
- package/test/unit/spec/meetings/index.js +653 -32
- package/test/unit/spec/member/index.js +28 -4
- package/test/unit/spec/member/util.js +65 -27
- package/test/unit/spec/multistream/mediaRequestManager.ts +2 -85
- package/test/unit/spec/multistream/remoteMediaManager.ts +30 -0
- package/test/unit/spec/reachability/index.ts +23 -0
- package/test/unit/spec/reconnection-manager/index.js +4 -8
- package/test/unit/spec/webinar/index.ts +474 -37
- package/test/unit/spec/webinar/utils.ts +39 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {assert
|
|
1
|
+
import {assert} from '@webex/test-helper-chai';
|
|
2
2
|
import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
|
|
3
3
|
import Webinar from '@webex/plugin-meetings/src/webinar';
|
|
4
4
|
import MockWebex from '@webex/test-helper-mock-webex';
|
|
5
5
|
import uuid from 'uuid';
|
|
6
6
|
import sinon from 'sinon';
|
|
7
|
+
import {DataChannelTokenType} from '@webex/internal-plugin-llm';
|
|
8
|
+
import {LLM_PRACTICE_SESSION, SHARE_STATUS} from '@webex/plugin-meetings/src/constants';
|
|
7
9
|
|
|
8
10
|
describe('plugin-meetings', () => {
|
|
9
11
|
describe('Webinar', () => {
|
|
@@ -26,7 +28,19 @@ describe('plugin-meetings', () => {
|
|
|
26
28
|
webex.meetings = {};
|
|
27
29
|
webex.credentials.getUserToken = getUserTokenStub;
|
|
28
30
|
webex.meetings.getMeetingByType = sinon.stub();
|
|
29
|
-
|
|
31
|
+
webex.internal.voicea.announce = sinon.stub();
|
|
32
|
+
|
|
33
|
+
webex.internal.llm = {
|
|
34
|
+
getDatachannelToken: sinon.stub().returns(undefined),
|
|
35
|
+
setDatachannelToken: sinon.stub(),
|
|
36
|
+
isConnected: sinon.stub().returns(false),
|
|
37
|
+
disconnectLLM: sinon.stub().resolves(),
|
|
38
|
+
off: sinon.stub(),
|
|
39
|
+
on: sinon.stub(),
|
|
40
|
+
getLocusUrl: sinon.stub().returns('old-locus-url'),
|
|
41
|
+
getDatachannelUrl: sinon.stub().returns('old-dc-url'),
|
|
42
|
+
registerAndConnect: sinon.stub().resolves('REGISTER_AND_CONNECT_RESULT'),
|
|
43
|
+
};
|
|
30
44
|
});
|
|
31
45
|
|
|
32
46
|
afterEach(() => {
|
|
@@ -147,20 +161,340 @@ describe('plugin-meetings', () => {
|
|
|
147
161
|
assert.equal(result.isPromoted, false, 'should not indicate promotion');
|
|
148
162
|
assert.equal(result.isDemoted, false, 'should not indicate demotion');
|
|
149
163
|
});
|
|
164
|
+
|
|
165
|
+
it('handles missing role payload safely', () => {
|
|
166
|
+
const updateStatusByRoleStub = sinon.stub(webinar, 'updateStatusByRole');
|
|
167
|
+
|
|
168
|
+
const result = webinar.updateRoleChanged(undefined);
|
|
169
|
+
|
|
170
|
+
assert.equal(webinar.selfIsPanelist, false);
|
|
171
|
+
assert.equal(webinar.selfIsAttendee, false);
|
|
172
|
+
assert.equal(webinar.canManageWebcast, false);
|
|
173
|
+
assert.deepEqual(result, {isPromoted: false, isDemoted: false});
|
|
174
|
+
assert.calledOnceWithExactly(updateStatusByRoleStub, {isPromoted: false, isDemoted: false});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('#cleanUp', () => {
|
|
179
|
+
it('delegates to cleanupPSDataChannel', () => {
|
|
180
|
+
const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
|
|
181
|
+
|
|
182
|
+
webinar.cleanUp();
|
|
183
|
+
|
|
184
|
+
assert.calledOnceWithExactly(cleanupPSDataChannelStub);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('#cleanupPSDataChannel', () => {
|
|
189
|
+
let meeting;
|
|
190
|
+
|
|
191
|
+
beforeEach(() => {
|
|
192
|
+
meeting = {
|
|
193
|
+
processRelayEvent: sinon.stub(),
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('disconnects the practice session channel and removes the relay listener', async () => {
|
|
200
|
+
await webinar.cleanupPSDataChannel();
|
|
201
|
+
|
|
202
|
+
assert.calledOnceWithExactly(
|
|
203
|
+
webex.internal.llm.disconnectLLM,
|
|
204
|
+
{code: 3050, reason: 'done (permanent)'},
|
|
205
|
+
LLM_PRACTICE_SESSION
|
|
206
|
+
);
|
|
207
|
+
assert.calledOnceWithExactly(
|
|
208
|
+
webex.internal.llm.off,
|
|
209
|
+
`event:relay.event:${LLM_PRACTICE_SESSION}`,
|
|
210
|
+
meeting.processRelayEvent
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('removes a pending online listener if one exists', async () => {
|
|
215
|
+
const listener = sinon.stub();
|
|
216
|
+
webinar._pendingOnlineListener = listener;
|
|
217
|
+
|
|
218
|
+
await webinar.cleanupPSDataChannel();
|
|
219
|
+
|
|
220
|
+
assert.calledWith(webex.internal.llm.off, 'online', listener);
|
|
221
|
+
assert.isNull(webinar._pendingOnlineListener);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('skips online listener removal when none is pending', async () => {
|
|
225
|
+
webinar._pendingOnlineListener = null;
|
|
226
|
+
|
|
227
|
+
await webinar.cleanupPSDataChannel();
|
|
228
|
+
|
|
229
|
+
// 'off' should only be called for the relay event, not for 'online'
|
|
230
|
+
const onlineOffCalls = webex.internal.llm.off.args.filter(([event]) => event === 'online');
|
|
231
|
+
assert.equal(onlineOffCalls.length, 0);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('#updatePSDataChannel', () => {
|
|
236
|
+
let meeting;
|
|
237
|
+
let processRelayEvent;
|
|
238
|
+
|
|
239
|
+
beforeEach(() => {
|
|
240
|
+
processRelayEvent = sinon.stub();
|
|
241
|
+
meeting = {
|
|
242
|
+
isJoined: sinon.stub().returns(true),
|
|
243
|
+
processRelayEvent,
|
|
244
|
+
locusInfo: {
|
|
245
|
+
url: 'locus-url',
|
|
246
|
+
info: {practiceSessionDatachannelUrl: 'dc-url'},
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
|
|
251
|
+
|
|
252
|
+
// Default session is connected by default; practice session is not
|
|
253
|
+
webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
|
|
254
|
+
return sessionId !== LLM_PRACTICE_SESSION;
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Token is pre-saved into LLM by saveDataChannelToken
|
|
258
|
+
webex.internal.llm.getDatachannelToken = sinon.stub().callsFake((tokenType) => {
|
|
259
|
+
if (tokenType === DataChannelTokenType.PracticeSession) return 'ps-token';
|
|
260
|
+
return undefined;
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Ensure connect path is eligible
|
|
264
|
+
webinar.selfIsPanelist = true;
|
|
265
|
+
webinar.practiceSessionEnabled = true;
|
|
266
|
+
webex.internal.voicea.getIsCaptionBoxOn = sinon.stub().returns(false);
|
|
267
|
+
webex.internal.voicea.updateSubchannelSubscriptions = sinon.stub();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('no-ops when practice session join eligibility is false', async () => {
|
|
271
|
+
webinar.practiceSessionEnabled = false;
|
|
272
|
+
const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
|
|
273
|
+
|
|
274
|
+
const result = await webinar.updatePSDataChannel();
|
|
275
|
+
|
|
276
|
+
assert.isUndefined(result);
|
|
277
|
+
assert.calledOnceWithExactly(cleanupPSDataChannelStub);
|
|
278
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('no-ops when meeting is not joined', async () => {
|
|
282
|
+
meeting.isJoined.returns(false);
|
|
283
|
+
const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
|
|
284
|
+
|
|
285
|
+
const result = await webinar.updatePSDataChannel();
|
|
286
|
+
|
|
287
|
+
assert.isUndefined(result);
|
|
288
|
+
assert.calledOnceWithExactly(cleanupPSDataChannelStub);
|
|
289
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('no-ops when practiceSessionDatachannelUrl is missing', async () => {
|
|
293
|
+
meeting.locusInfo.info.practiceSessionDatachannelUrl = undefined;
|
|
294
|
+
|
|
295
|
+
const result = await webinar.updatePSDataChannel();
|
|
296
|
+
|
|
297
|
+
assert.isUndefined(result);
|
|
298
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('no-ops when already connected to the same endpoints', async () => {
|
|
302
|
+
webex.internal.llm.isConnected.returns(true);
|
|
303
|
+
webex.internal.llm.getLocusUrl.returns('locus-url');
|
|
304
|
+
webex.internal.llm.getDatachannelUrl.returns('dc-url');
|
|
305
|
+
const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
|
|
306
|
+
|
|
307
|
+
const result = await webinar.updatePSDataChannel();
|
|
308
|
+
|
|
309
|
+
assert.isUndefined(result);
|
|
310
|
+
assert.notCalled(cleanupPSDataChannelStub);
|
|
311
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('connects when eligible', async () => {
|
|
315
|
+
const result = await webinar.updatePSDataChannel();
|
|
316
|
+
|
|
317
|
+
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
318
|
+
assert.calledWith(
|
|
319
|
+
webex.internal.llm.registerAndConnect,
|
|
320
|
+
'locus-url',
|
|
321
|
+
'dc-url',
|
|
322
|
+
'ps-token',
|
|
323
|
+
LLM_PRACTICE_SESSION
|
|
324
|
+
);
|
|
325
|
+
assert.calledOnceWithExactly(webex.internal.voicea.announce);
|
|
326
|
+
assert.equal(result, 'REGISTER_AND_CONNECT_RESULT');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('uses token from LLM', async () => {
|
|
330
|
+
webex.internal.llm.getDatachannelToken = sinon.stub().callsFake((tokenType) => {
|
|
331
|
+
if (tokenType === DataChannelTokenType.PracticeSession) return 'cached-token';
|
|
332
|
+
return undefined;
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
await webinar.updatePSDataChannel();
|
|
336
|
+
|
|
337
|
+
assert.calledWithExactly(
|
|
338
|
+
webex.internal.llm.getDatachannelToken,
|
|
339
|
+
DataChannelTokenType.PracticeSession
|
|
340
|
+
);
|
|
341
|
+
assert.notCalled(webex.internal.llm.setDatachannelToken);
|
|
342
|
+
assert.calledWith(
|
|
343
|
+
webex.internal.llm.registerAndConnect,
|
|
344
|
+
'locus-url',
|
|
345
|
+
'dc-url',
|
|
346
|
+
'cached-token',
|
|
347
|
+
LLM_PRACTICE_SESSION
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('cleans up the existing practice session channel before reconnecting to new endpoints', async () => {
|
|
352
|
+
webex.internal.llm.isConnected.returns(true);
|
|
353
|
+
const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
|
|
354
|
+
|
|
355
|
+
await webinar.updatePSDataChannel();
|
|
356
|
+
|
|
357
|
+
assert.calledOnceWithExactly(cleanupPSDataChannelStub);
|
|
358
|
+
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('rebinds relay listener after successful connect', async () => {
|
|
362
|
+
await webinar.updatePSDataChannel();
|
|
363
|
+
|
|
364
|
+
assert.calledWith(
|
|
365
|
+
webex.internal.llm.off,
|
|
366
|
+
`event:relay.event:${LLM_PRACTICE_SESSION}`,
|
|
367
|
+
processRelayEvent
|
|
368
|
+
);
|
|
369
|
+
assert.calledWith(
|
|
370
|
+
webex.internal.llm.on,
|
|
371
|
+
`event:relay.event:${LLM_PRACTICE_SESSION}`,
|
|
372
|
+
processRelayEvent
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('subscribes to transcription when caption intent is enabled', async () => {
|
|
377
|
+
webex.internal.voicea.getIsCaptionBoxOn = sinon.stub().returns(true);
|
|
378
|
+
|
|
379
|
+
await webinar.updatePSDataChannel();
|
|
380
|
+
|
|
381
|
+
assert.calledOnceWithExactly(webex.internal.voicea.updateSubchannelSubscriptions, { subscribe: ['transcription'] });
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('does not subscribe to transcription when caption intent is disabled', async () => {
|
|
385
|
+
webex.internal.voicea.getIsCaptionBoxOn = sinon.stub().returns(false);
|
|
386
|
+
|
|
387
|
+
await webinar.updatePSDataChannel();
|
|
388
|
+
|
|
389
|
+
assert.notCalled(webex.internal.voicea.updateSubchannelSubscriptions);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('defers connect when default session is not yet connected', async () => {
|
|
393
|
+
// Default session is not connected initially
|
|
394
|
+
webex.internal.llm.isConnected = sinon.stub().returns(false);
|
|
395
|
+
|
|
396
|
+
const result = await webinar.updatePSDataChannel();
|
|
397
|
+
|
|
398
|
+
// Should return undefined immediately (deferred)
|
|
399
|
+
assert.isUndefined(result);
|
|
400
|
+
// Should register an 'online' listener but NOT call registerAndConnect yet
|
|
401
|
+
assert.calledWith(webex.internal.llm.on, 'online', sinon.match.func);
|
|
402
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
403
|
+
// Should store the pending listener
|
|
404
|
+
assert.isNotNull(webinar._pendingOnlineListener);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('does not register duplicate online listeners on repeated calls', async () => {
|
|
408
|
+
webex.internal.llm.isConnected = sinon.stub().returns(false);
|
|
409
|
+
|
|
410
|
+
await webinar.updatePSDataChannel();
|
|
411
|
+
await webinar.updatePSDataChannel();
|
|
412
|
+
await webinar.updatePSDataChannel();
|
|
413
|
+
|
|
414
|
+
// Only one 'online' listener should have been registered
|
|
415
|
+
const onlineCalls = webex.internal.llm.on.args.filter(([event]) => event === 'online');
|
|
416
|
+
assert.equal(onlineCalls.length, 1, 'should register exactly one online listener');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('re-invokes updatePSDataChannel when default session comes online', async () => {
|
|
420
|
+
// Default session is not connected initially
|
|
421
|
+
webex.internal.llm.isConnected = sinon.stub().returns(false);
|
|
422
|
+
|
|
423
|
+
const updatePSDataChannelSpy = sinon.spy(webinar, 'updatePSDataChannel');
|
|
424
|
+
|
|
425
|
+
// First call defers
|
|
426
|
+
await webinar.updatePSDataChannel();
|
|
427
|
+
|
|
428
|
+
// Capture the 'online' listener
|
|
429
|
+
const onlineCall = webex.internal.llm.on.args.find(([event]) => event === 'online');
|
|
430
|
+
assert.isDefined(onlineCall, 'should have registered an online listener');
|
|
431
|
+
|
|
432
|
+
// Now simulate default session coming online
|
|
433
|
+
webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
|
|
434
|
+
return sessionId !== LLM_PRACTICE_SESSION;
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Fire the captured listener
|
|
438
|
+
onlineCall[1]();
|
|
439
|
+
|
|
440
|
+
// The listener should have cleared itself, removed itself, and re-called updatePSDataChannel
|
|
441
|
+
assert.isNull(webinar._pendingOnlineListener);
|
|
442
|
+
assert.calledWith(webex.internal.llm.off, 'online', sinon.match.func);
|
|
443
|
+
assert.equal(updatePSDataChannelSpy.callCount, 2);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('does not reconnect with stale data if demoted before default session comes online', async () => {
|
|
447
|
+
// Default session is not connected initially
|
|
448
|
+
webex.internal.llm.isConnected = sinon.stub().returns(false);
|
|
449
|
+
|
|
450
|
+
await webinar.updatePSDataChannel();
|
|
451
|
+
|
|
452
|
+
// Capture the 'online' listener
|
|
453
|
+
const onlineCall = webex.internal.llm.on.args.find(([event]) => event === 'online');
|
|
454
|
+
assert.isDefined(onlineCall);
|
|
455
|
+
|
|
456
|
+
// Simulate demotion while waiting
|
|
457
|
+
webinar.selfIsPanelist = false;
|
|
458
|
+
|
|
459
|
+
// Now default session comes online
|
|
460
|
+
webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
|
|
461
|
+
return sessionId !== LLM_PRACTICE_SESSION;
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Fire the listener — re-invokes updatePSDataChannel which will see isPracticeSession = false
|
|
465
|
+
onlineCall[1]();
|
|
466
|
+
|
|
467
|
+
// Should NOT have called registerAndConnect since the user is no longer eligible
|
|
468
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('proceeds immediately when default session is already connected', async () => {
|
|
472
|
+
// Default session already connected, practice session not
|
|
473
|
+
webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
|
|
474
|
+
return sessionId !== LLM_PRACTICE_SESSION;
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
const result = await webinar.updatePSDataChannel();
|
|
478
|
+
|
|
479
|
+
// The 'online' listener is registered then immediately removed since default session is already connected
|
|
480
|
+
assert.calledWith(webex.internal.llm.on, 'online', sinon.match.func);
|
|
481
|
+
assert.calledWith(webex.internal.llm.off, 'online', sinon.match.func);
|
|
482
|
+
assert.isNull(webinar._pendingOnlineListener);
|
|
483
|
+
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
484
|
+
assert.equal(result, 'REGISTER_AND_CONNECT_RESULT');
|
|
485
|
+
});
|
|
150
486
|
});
|
|
151
487
|
|
|
152
488
|
describe('#updateStatusByRole', () => {
|
|
153
|
-
let updateLLMConnection;
|
|
154
489
|
let updateMediaShares;
|
|
155
490
|
beforeEach(() => {
|
|
156
|
-
// @ts-ignore
|
|
157
|
-
updateLLMConnection = sinon.stub();
|
|
158
491
|
updateMediaShares = sinon.stub()
|
|
159
492
|
webinar.webex.meetings = {
|
|
160
493
|
getMeetingByType: sinon.stub().returns({
|
|
161
494
|
id: 'meeting-id',
|
|
162
|
-
|
|
163
|
-
|
|
495
|
+
isJoined: sinon.stub().returns(false),
|
|
496
|
+
updateLLMConnection: sinon.stub(),
|
|
497
|
+
shareStatus: SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE,
|
|
164
498
|
locusInfo: {
|
|
165
499
|
mediaShares: 'mediaShares',
|
|
166
500
|
updateMediaShares: updateMediaShares
|
|
@@ -173,40 +507,20 @@ describe('plugin-meetings', () => {
|
|
|
173
507
|
sinon.restore();
|
|
174
508
|
});
|
|
175
509
|
|
|
176
|
-
it('trigger updateLLMConnection if PS started', () => {
|
|
177
|
-
|
|
178
|
-
webinar.practiceSessionEnabled = true;
|
|
179
|
-
const roleChange = {isPromoted: true, isDemoted: false};
|
|
180
|
-
|
|
181
|
-
const result = webinar.updateStatusByRole(roleChange);
|
|
182
|
-
|
|
183
|
-
assert.calledOnce(updateLLMConnection);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('Not trigger updateLLMConnection if PS not started', () => {
|
|
187
|
-
|
|
188
|
-
webinar.practiceSessionEnabled = false;
|
|
189
|
-
const roleChange = {isPromoted: true, isDemoted: false};
|
|
190
|
-
|
|
191
|
-
const result = webinar.updateStatusByRole(roleChange);
|
|
192
|
-
|
|
193
|
-
assert.notCalled(updateLLMConnection);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
510
|
it('trigger updateMediaShares if promoted', () => {
|
|
197
511
|
|
|
198
512
|
const roleChange = {isPromoted: true, isDemoted: false};
|
|
199
513
|
|
|
200
|
-
|
|
514
|
+
webinar.updateStatusByRole(roleChange);
|
|
201
515
|
|
|
202
|
-
assert.
|
|
516
|
+
assert.calledOnceWithExactly(updateMediaShares, 'mediaShares', true);
|
|
203
517
|
});
|
|
204
518
|
|
|
205
519
|
it('Not trigger updateMediaShares if no role change', () => {
|
|
206
520
|
|
|
207
521
|
const roleChange = {isPromoted: false, isDemoted: false};
|
|
208
522
|
|
|
209
|
-
|
|
523
|
+
webinar.updateStatusByRole(roleChange);
|
|
210
524
|
|
|
211
525
|
assert.notCalled(updateMediaShares);
|
|
212
526
|
});
|
|
@@ -214,18 +528,18 @@ describe('plugin-meetings', () => {
|
|
|
214
528
|
|
|
215
529
|
const roleChange = {isPromoted: true, isDemoted: false};
|
|
216
530
|
|
|
217
|
-
|
|
531
|
+
webinar.updateStatusByRole(roleChange);
|
|
218
532
|
|
|
219
|
-
assert.
|
|
533
|
+
assert.calledOnceWithExactly(updateMediaShares, 'mediaShares', true);
|
|
220
534
|
});
|
|
221
535
|
|
|
222
536
|
it('trigger updateMediaShares if is attendee with whiteboard share', () => {
|
|
223
537
|
|
|
224
538
|
const roleChange = {isPromoted: false, isDemoted: true};
|
|
225
539
|
|
|
226
|
-
|
|
540
|
+
webinar.updateStatusByRole(roleChange);
|
|
227
541
|
|
|
228
|
-
assert.
|
|
542
|
+
assert.calledOnceWithExactly(updateMediaShares, 'mediaShares', true);
|
|
229
543
|
});
|
|
230
544
|
|
|
231
545
|
it('Not trigger updateMediaShares if is attendee with screen share', () => {
|
|
@@ -233,8 +547,9 @@ describe('plugin-meetings', () => {
|
|
|
233
547
|
webinar.webex.meetings = {
|
|
234
548
|
getMeetingByType: sinon.stub().returns({
|
|
235
549
|
id: 'meeting-id',
|
|
236
|
-
|
|
237
|
-
|
|
550
|
+
isJoined: sinon.stub().returns(false),
|
|
551
|
+
updateLLMConnection: sinon.stub(),
|
|
552
|
+
shareStatus: SHARE_STATUS.REMOTE_SHARE_ACTIVE,
|
|
238
553
|
locusInfo: {
|
|
239
554
|
mediaShares: 'mediaShares',
|
|
240
555
|
updateMediaShares: updateMediaShares
|
|
@@ -244,10 +559,18 @@ describe('plugin-meetings', () => {
|
|
|
244
559
|
|
|
245
560
|
const roleChange = {isPromoted: false, isDemoted: true};
|
|
246
561
|
|
|
247
|
-
|
|
562
|
+
webinar.updateStatusByRole(roleChange);
|
|
248
563
|
|
|
249
564
|
assert.notCalled(updateMediaShares);
|
|
250
565
|
});
|
|
566
|
+
|
|
567
|
+
it('updates PS data channel based on join eligibility', () => {
|
|
568
|
+
const updatePSDataChannelStub = sinon.stub(webinar, 'updatePSDataChannel').resolves();
|
|
569
|
+
|
|
570
|
+
webinar.updateStatusByRole({isPromoted: false, isDemoted: false});
|
|
571
|
+
|
|
572
|
+
assert.calledOnceWithExactly(updatePSDataChannelStub);
|
|
573
|
+
});
|
|
251
574
|
});
|
|
252
575
|
|
|
253
576
|
describe("#setPracticeSessionState", () => {
|
|
@@ -323,6 +646,14 @@ describe('plugin-meetings', () => {
|
|
|
323
646
|
|
|
324
647
|
assert.equal(webinar.practiceSessionEnabled, false);
|
|
325
648
|
});
|
|
649
|
+
it('triggers PS data channel update using computed eligibility', () => {
|
|
650
|
+
webinar.selfIsPanelist = true;
|
|
651
|
+
const updatePSDataChannelStub = sinon.stub(webinar, 'updatePSDataChannel').resolves();
|
|
652
|
+
|
|
653
|
+
webinar.updatePracticeSessionStatus({enabled: true});
|
|
654
|
+
|
|
655
|
+
assert.calledOnceWithExactly(updatePSDataChannelStub);
|
|
656
|
+
});
|
|
326
657
|
});
|
|
327
658
|
|
|
328
659
|
describe("#startWebcast", () => {
|
|
@@ -631,5 +962,111 @@ describe('plugin-meetings', () => {
|
|
|
631
962
|
}
|
|
632
963
|
});
|
|
633
964
|
});
|
|
965
|
+
|
|
966
|
+
describe("#searchLargeScaleWebinarAttendees", () => {
|
|
967
|
+
const attendeeSearchUrl = 'https://locusUrl/attendees/search';
|
|
968
|
+
const params = {
|
|
969
|
+
queryString: 'queryString',
|
|
970
|
+
limit: 50,
|
|
971
|
+
next: null,
|
|
972
|
+
};
|
|
973
|
+
beforeEach(() => {
|
|
974
|
+
// @ts-ignore
|
|
975
|
+
webinar.webex.meetings = {
|
|
976
|
+
getMeetingByType: sinon.stub().returns({
|
|
977
|
+
id: 'meeting-id',
|
|
978
|
+
locusInfo: {
|
|
979
|
+
links:{
|
|
980
|
+
resources: {
|
|
981
|
+
attendeeSearch: {
|
|
982
|
+
url: attendeeSearchUrl
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
})
|
|
988
|
+
};
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
it('throws an error if attendeeSearchUrl is not available', async () => {
|
|
992
|
+
webinar.webex.meetings = {
|
|
993
|
+
getMeetingByType: sinon.stub().returns({
|
|
994
|
+
id: 'meeting-id',
|
|
995
|
+
locusInfo: {
|
|
996
|
+
links:{
|
|
997
|
+
resources: {
|
|
998
|
+
attendeeSearch: {
|
|
999
|
+
url: null
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
})
|
|
1005
|
+
};
|
|
1006
|
+
try {
|
|
1007
|
+
await webinar.searchLargeScaleWebinarAttendees(params);
|
|
1008
|
+
assert.fail('searchLargeScaleWebinarAttendees should throw an error');
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
assert.equal(error.message,'Meeting:webinar5k#Attendee search url is not available', 'should throw the correct error');
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
it('sends a GET request to search the large scale webinar attendees', async () => {
|
|
1015
|
+
const result = await webinar.searchLargeScaleWebinarAttendees(params);
|
|
1016
|
+
assert.calledOnce(webex.request);
|
|
1017
|
+
assert.calledWith(webex.request, {
|
|
1018
|
+
method: 'GET',
|
|
1019
|
+
uri: `${attendeeSearchUrl}?search_text=${encodeURIComponent(params.queryString)}&limit=50`,
|
|
1020
|
+
headers: {
|
|
1021
|
+
authorization: 'test-token',
|
|
1022
|
+
trackingId: 'webex-js-sdk_test-uuid',
|
|
1023
|
+
},
|
|
1024
|
+
});
|
|
1025
|
+
assert.equal(
|
|
1026
|
+
result,
|
|
1027
|
+
'REQUEST_RETURN_VALUE',
|
|
1028
|
+
'should return the resolved value from the request'
|
|
1029
|
+
);
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
it('queryString is empty string', async () => {
|
|
1033
|
+
params.queryString = '';
|
|
1034
|
+
const result = await webinar.searchLargeScaleWebinarAttendees(params);
|
|
1035
|
+
assert.calledOnce(webex.request);
|
|
1036
|
+
assert.calledWith(webex.request, {
|
|
1037
|
+
method: 'GET',
|
|
1038
|
+
uri: `${attendeeSearchUrl}?limit=50`,
|
|
1039
|
+
headers: {
|
|
1040
|
+
authorization: 'test-token',
|
|
1041
|
+
trackingId: 'webex-js-sdk_test-uuid',
|
|
1042
|
+
},
|
|
1043
|
+
});
|
|
1044
|
+
assert.equal(
|
|
1045
|
+
result,
|
|
1046
|
+
'REQUEST_RETURN_VALUE',
|
|
1047
|
+
'should return the resolved value from the request'
|
|
1048
|
+
);
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
it('handles API call failures gracefully', async () => {
|
|
1052
|
+
webex.request.rejects(new Error('API_ERROR'));
|
|
1053
|
+
const errorLogger = sinon.stub(LoggerProxy.logger, 'error');
|
|
1054
|
+
|
|
1055
|
+
try {
|
|
1056
|
+
await webinar.searchLargeScaleWebinarAttendees(params);
|
|
1057
|
+
assert.fail('searchLargeScaleWebinarAttendees should throw an error');
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
assert.equal(error.message, 'API_ERROR', 'should throw the correct error');
|
|
1060
|
+
assert.calledOnce(errorLogger);
|
|
1061
|
+
assert.calledWith(
|
|
1062
|
+
errorLogger,
|
|
1063
|
+
'Meeting:webinar5k#searchLargeScaleWebinarAttendees failed',
|
|
1064
|
+
sinon.match.instanceOf(Error)
|
|
1065
|
+
);
|
|
1066
|
+
} finally {
|
|
1067
|
+
errorLogger.restore();
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
});
|
|
634
1071
|
})
|
|
635
1072
|
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import chai from 'chai';
|
|
2
|
+
import {sanitizeParams} from '@webex/plugin-meetings/src/webinar/utils';
|
|
3
|
+
|
|
4
|
+
const {assert} = chai;
|
|
5
|
+
|
|
6
|
+
describe('plugin-meetings', () => {
|
|
7
|
+
describe('webinar utils', () => {
|
|
8
|
+
describe('#sanitizeParams', () => {
|
|
9
|
+
it('sanitizes params by removing undefined, "", or null values', () => {
|
|
10
|
+
const input = {
|
|
11
|
+
a: 1,
|
|
12
|
+
b: undefined,
|
|
13
|
+
c: null,
|
|
14
|
+
d: 'test',
|
|
15
|
+
e: false,
|
|
16
|
+
f: '',
|
|
17
|
+
};
|
|
18
|
+
const expectedOutput = {
|
|
19
|
+
a: 1,
|
|
20
|
+
d: 'test',
|
|
21
|
+
e: false,
|
|
22
|
+
};
|
|
23
|
+
const result = sanitizeParams(input);
|
|
24
|
+
assert.deepEqual(result, expectedOutput);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns an empty object when all values are invalid', () => {
|
|
28
|
+
const input = {
|
|
29
|
+
a: undefined,
|
|
30
|
+
b: null,
|
|
31
|
+
c: '',
|
|
32
|
+
};
|
|
33
|
+
const expectedOutput = {};
|
|
34
|
+
const result = sanitizeParams(input);
|
|
35
|
+
assert.deepEqual(result, expectedOutput);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|