@webex/plugin-meetings 3.11.0 → 3.12.0-mobius-socket.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 +868 -419
- 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 +1304 -928
- 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 +6 -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/multistream/sendSlotManager.js +116 -2
- package/dist/multistream/sendSlotManager.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 +72 -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 +5 -0
- package/dist/types/multistream/mediaRequestManager.d.ts +0 -23
- package/dist/types/multistream/sendSlotManager.d.ts +23 -1
- package/dist/types/reactions/reactions.type.d.ts +1 -0
- package/dist/types/webinar/utils.d.ts +6 -0
- package/dist/webinar/index.js +438 -163
- 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 +761 -260
- 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 +389 -87
- 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 +6 -0
- package/src/multistream/mediaRequestManager.ts +4 -54
- package/src/multistream/remoteMediaManager.ts +13 -0
- package/src/multistream/sendSlotManager.ts +97 -3
- 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 +265 -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 +2321 -175
- 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 +884 -152
- 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/multistream/sendSlotManager.ts +135 -36
- 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 +534 -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,20 @@ 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
|
+
isDataChannelTokenEnabled: sinon.stub().resolves(false),
|
|
37
|
+
isConnected: sinon.stub().returns(false),
|
|
38
|
+
disconnectLLM: sinon.stub().resolves(),
|
|
39
|
+
off: sinon.stub(),
|
|
40
|
+
on: sinon.stub(),
|
|
41
|
+
getLocusUrl: sinon.stub().returns('old-locus-url'),
|
|
42
|
+
getDatachannelUrl: sinon.stub().returns('old-dc-url'),
|
|
43
|
+
registerAndConnect: sinon.stub().resolves('REGISTER_AND_CONNECT_RESULT'),
|
|
44
|
+
};
|
|
30
45
|
});
|
|
31
46
|
|
|
32
47
|
afterEach(() => {
|
|
@@ -147,20 +162,399 @@ describe('plugin-meetings', () => {
|
|
|
147
162
|
assert.equal(result.isPromoted, false, 'should not indicate promotion');
|
|
148
163
|
assert.equal(result.isDemoted, false, 'should not indicate demotion');
|
|
149
164
|
});
|
|
165
|
+
|
|
166
|
+
it('handles missing role payload safely', () => {
|
|
167
|
+
const updateStatusByRoleStub = sinon.stub(webinar, 'updateStatusByRole');
|
|
168
|
+
|
|
169
|
+
const result = webinar.updateRoleChanged(undefined);
|
|
170
|
+
|
|
171
|
+
assert.equal(webinar.selfIsPanelist, false);
|
|
172
|
+
assert.equal(webinar.selfIsAttendee, false);
|
|
173
|
+
assert.equal(webinar.canManageWebcast, false);
|
|
174
|
+
assert.deepEqual(result, {isPromoted: false, isDemoted: false});
|
|
175
|
+
assert.calledOnceWithExactly(updateStatusByRoleStub, {isPromoted: false, isDemoted: false});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('#cleanUp', () => {
|
|
180
|
+
it('delegates to cleanupPSDataChannel', () => {
|
|
181
|
+
const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
|
|
182
|
+
|
|
183
|
+
webinar.cleanUp();
|
|
184
|
+
|
|
185
|
+
assert.calledOnceWithExactly(cleanupPSDataChannelStub);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('#cleanupPSDataChannel', () => {
|
|
190
|
+
let meeting;
|
|
191
|
+
|
|
192
|
+
beforeEach(() => {
|
|
193
|
+
meeting = {
|
|
194
|
+
processRelayEvent: sinon.stub(),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('disconnects the practice session channel and removes the relay listener', async () => {
|
|
201
|
+
await webinar.cleanupPSDataChannel();
|
|
202
|
+
|
|
203
|
+
assert.calledOnceWithExactly(
|
|
204
|
+
webex.internal.llm.disconnectLLM,
|
|
205
|
+
{code: 3050, reason: 'done (permanent)'},
|
|
206
|
+
LLM_PRACTICE_SESSION
|
|
207
|
+
);
|
|
208
|
+
assert.calledOnceWithExactly(
|
|
209
|
+
webex.internal.llm.off,
|
|
210
|
+
`event:relay.event:${LLM_PRACTICE_SESSION}`,
|
|
211
|
+
meeting.processRelayEvent
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('removes a pending online listener if one exists', async () => {
|
|
216
|
+
const listener = sinon.stub();
|
|
217
|
+
webinar._pendingOnlineListener = listener;
|
|
218
|
+
|
|
219
|
+
await webinar.cleanupPSDataChannel();
|
|
220
|
+
|
|
221
|
+
assert.calledWith(webex.internal.llm.off, 'online', listener);
|
|
222
|
+
assert.isNull(webinar._pendingOnlineListener);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('skips online listener removal when none is pending', async () => {
|
|
226
|
+
webinar._pendingOnlineListener = null;
|
|
227
|
+
|
|
228
|
+
await webinar.cleanupPSDataChannel();
|
|
229
|
+
|
|
230
|
+
// 'off' should only be called for the relay event, not for 'online'
|
|
231
|
+
const onlineOffCalls = webex.internal.llm.off.args.filter(([event]) => event === 'online');
|
|
232
|
+
assert.equal(onlineOffCalls.length, 0);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('#updatePSDataChannel', () => {
|
|
237
|
+
let meeting;
|
|
238
|
+
let processRelayEvent;
|
|
239
|
+
|
|
240
|
+
beforeEach(() => {
|
|
241
|
+
processRelayEvent = sinon.stub();
|
|
242
|
+
meeting = {
|
|
243
|
+
isJoined: sinon.stub().returns(true),
|
|
244
|
+
processRelayEvent,
|
|
245
|
+
locusInfo: {
|
|
246
|
+
url: 'locus-url',
|
|
247
|
+
info: {practiceSessionDatachannelUrl: 'dc-url'},
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
|
|
252
|
+
|
|
253
|
+
// Default session is connected by default; practice session is not
|
|
254
|
+
webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
|
|
255
|
+
return sessionId !== LLM_PRACTICE_SESSION;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Token is pre-saved into LLM by saveDataChannelToken
|
|
259
|
+
webex.internal.llm.getDatachannelToken = sinon.stub().callsFake((tokenType) => {
|
|
260
|
+
if (tokenType === DataChannelTokenType.PracticeSession) return 'ps-token';
|
|
261
|
+
return undefined;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Ensure connect path is eligible
|
|
265
|
+
webinar.selfIsPanelist = true;
|
|
266
|
+
webinar.practiceSessionEnabled = true;
|
|
267
|
+
webex.internal.voicea.getIsCaptionBoxOn = sinon.stub().returns(false);
|
|
268
|
+
webex.internal.voicea.updateSubchannelSubscriptions = sinon.stub();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('refreshes practice-session token before register when cached token is missing', async () => {
|
|
272
|
+
webex.internal.llm.isDataChannelTokenEnabled.resolves(true);
|
|
273
|
+
webex.internal.llm.getDatachannelToken = sinon.stub().callsFake((tokenType) => {
|
|
274
|
+
if (tokenType === DataChannelTokenType.PracticeSession) return undefined;
|
|
275
|
+
|
|
276
|
+
return undefined;
|
|
277
|
+
});
|
|
278
|
+
meeting.refreshDataChannelToken = sinon.stub().resolves({
|
|
279
|
+
body: {
|
|
280
|
+
datachannelToken: 'ps-token-from-refresh',
|
|
281
|
+
dataChannelTokenType: DataChannelTokenType.PracticeSession,
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
await webinar.updatePSDataChannel();
|
|
286
|
+
|
|
287
|
+
assert.calledOnceWithExactly(meeting.refreshDataChannelToken);
|
|
288
|
+
assert.calledWithExactly(
|
|
289
|
+
webex.internal.llm.setDatachannelToken,
|
|
290
|
+
'ps-token-from-refresh',
|
|
291
|
+
DataChannelTokenType.PracticeSession
|
|
292
|
+
);
|
|
293
|
+
assert.calledWith(
|
|
294
|
+
webex.internal.llm.registerAndConnect,
|
|
295
|
+
'locus-url',
|
|
296
|
+
'dc-url',
|
|
297
|
+
'ps-token-from-refresh',
|
|
298
|
+
LLM_PRACTICE_SESSION
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('does not reconnect if practice-session eligibility changes during async token refresh', async () => {
|
|
303
|
+
webex.internal.llm.isDataChannelTokenEnabled.resolves(true);
|
|
304
|
+
webex.internal.llm.getDatachannelToken = sinon.stub().returns(undefined);
|
|
305
|
+
|
|
306
|
+
let resolveRefresh;
|
|
307
|
+
meeting.refreshDataChannelToken = sinon.stub().returns(
|
|
308
|
+
new Promise((resolve) => {
|
|
309
|
+
resolveRefresh = resolve;
|
|
310
|
+
})
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const updatePromise = webinar.updatePSDataChannel();
|
|
314
|
+
|
|
315
|
+
webinar.practiceSessionEnabled = false;
|
|
316
|
+
|
|
317
|
+
resolveRefresh({
|
|
318
|
+
body: {
|
|
319
|
+
datachannelToken: 'stale-ps-token',
|
|
320
|
+
dataChannelTokenType: DataChannelTokenType.PracticeSession,
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const result = await updatePromise;
|
|
325
|
+
|
|
326
|
+
assert.isUndefined(result);
|
|
327
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('no-ops when practice session join eligibility is false', async () => {
|
|
331
|
+
webinar.practiceSessionEnabled = false;
|
|
332
|
+
const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
|
|
333
|
+
|
|
334
|
+
const result = await webinar.updatePSDataChannel();
|
|
335
|
+
|
|
336
|
+
assert.isUndefined(result);
|
|
337
|
+
assert.calledOnceWithExactly(cleanupPSDataChannelStub);
|
|
338
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('no-ops when meeting is not joined', async () => {
|
|
342
|
+
meeting.isJoined.returns(false);
|
|
343
|
+
const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
|
|
344
|
+
|
|
345
|
+
const result = await webinar.updatePSDataChannel();
|
|
346
|
+
|
|
347
|
+
assert.isUndefined(result);
|
|
348
|
+
assert.calledOnceWithExactly(cleanupPSDataChannelStub);
|
|
349
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('no-ops when practiceSessionDatachannelUrl is missing', async () => {
|
|
353
|
+
meeting.locusInfo.info.practiceSessionDatachannelUrl = undefined;
|
|
354
|
+
|
|
355
|
+
const result = await webinar.updatePSDataChannel();
|
|
356
|
+
|
|
357
|
+
assert.isUndefined(result);
|
|
358
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('no-ops when already connected to the same endpoints', async () => {
|
|
362
|
+
webex.internal.llm.isConnected.returns(true);
|
|
363
|
+
webex.internal.llm.getLocusUrl.returns('locus-url');
|
|
364
|
+
webex.internal.llm.getDatachannelUrl.returns('dc-url');
|
|
365
|
+
const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
|
|
366
|
+
|
|
367
|
+
const result = await webinar.updatePSDataChannel();
|
|
368
|
+
|
|
369
|
+
assert.isUndefined(result);
|
|
370
|
+
assert.notCalled(cleanupPSDataChannelStub);
|
|
371
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('connects when eligible', async () => {
|
|
375
|
+
const result = await webinar.updatePSDataChannel();
|
|
376
|
+
|
|
377
|
+
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
378
|
+
assert.calledWith(
|
|
379
|
+
webex.internal.llm.registerAndConnect,
|
|
380
|
+
'locus-url',
|
|
381
|
+
'dc-url',
|
|
382
|
+
'ps-token',
|
|
383
|
+
LLM_PRACTICE_SESSION
|
|
384
|
+
);
|
|
385
|
+
assert.calledOnceWithExactly(webex.internal.voicea.announce);
|
|
386
|
+
assert.equal(result, 'REGISTER_AND_CONNECT_RESULT');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('uses token from LLM', async () => {
|
|
390
|
+
webex.internal.llm.getDatachannelToken = sinon.stub().callsFake((tokenType) => {
|
|
391
|
+
if (tokenType === DataChannelTokenType.PracticeSession) return 'cached-token';
|
|
392
|
+
return undefined;
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
await webinar.updatePSDataChannel();
|
|
396
|
+
|
|
397
|
+
assert.calledWithExactly(
|
|
398
|
+
webex.internal.llm.getDatachannelToken,
|
|
399
|
+
DataChannelTokenType.PracticeSession
|
|
400
|
+
);
|
|
401
|
+
assert.notCalled(webex.internal.llm.setDatachannelToken);
|
|
402
|
+
assert.calledWith(
|
|
403
|
+
webex.internal.llm.registerAndConnect,
|
|
404
|
+
'locus-url',
|
|
405
|
+
'dc-url',
|
|
406
|
+
'cached-token',
|
|
407
|
+
LLM_PRACTICE_SESSION
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('cleans up the existing practice session channel before reconnecting to new endpoints', async () => {
|
|
412
|
+
webex.internal.llm.isConnected.returns(true);
|
|
413
|
+
const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
|
|
414
|
+
|
|
415
|
+
await webinar.updatePSDataChannel();
|
|
416
|
+
|
|
417
|
+
assert.calledOnceWithExactly(cleanupPSDataChannelStub);
|
|
418
|
+
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('rebinds relay listener after successful connect', async () => {
|
|
422
|
+
await webinar.updatePSDataChannel();
|
|
423
|
+
|
|
424
|
+
assert.calledWith(
|
|
425
|
+
webex.internal.llm.off,
|
|
426
|
+
`event:relay.event:${LLM_PRACTICE_SESSION}`,
|
|
427
|
+
processRelayEvent
|
|
428
|
+
);
|
|
429
|
+
assert.calledWith(
|
|
430
|
+
webex.internal.llm.on,
|
|
431
|
+
`event:relay.event:${LLM_PRACTICE_SESSION}`,
|
|
432
|
+
processRelayEvent
|
|
433
|
+
);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('subscribes to transcription when caption intent is enabled', async () => {
|
|
437
|
+
webex.internal.voicea.getIsCaptionBoxOn = sinon.stub().returns(true);
|
|
438
|
+
|
|
439
|
+
await webinar.updatePSDataChannel();
|
|
440
|
+
|
|
441
|
+
assert.calledOnceWithExactly(webex.internal.voicea.updateSubchannelSubscriptions, { subscribe: ['transcription'] });
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('does not subscribe to transcription when caption intent is disabled', async () => {
|
|
445
|
+
webex.internal.voicea.getIsCaptionBoxOn = sinon.stub().returns(false);
|
|
446
|
+
|
|
447
|
+
await webinar.updatePSDataChannel();
|
|
448
|
+
|
|
449
|
+
assert.notCalled(webex.internal.voicea.updateSubchannelSubscriptions);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('defers connect when default session is not yet connected', async () => {
|
|
453
|
+
// Default session is not connected initially
|
|
454
|
+
webex.internal.llm.isConnected = sinon.stub().returns(false);
|
|
455
|
+
|
|
456
|
+
const result = await webinar.updatePSDataChannel();
|
|
457
|
+
|
|
458
|
+
// Should return undefined immediately (deferred)
|
|
459
|
+
assert.isUndefined(result);
|
|
460
|
+
// Should register an 'online' listener but NOT call registerAndConnect yet
|
|
461
|
+
assert.calledWith(webex.internal.llm.on, 'online', sinon.match.func);
|
|
462
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
463
|
+
// Should store the pending listener
|
|
464
|
+
assert.isNotNull(webinar._pendingOnlineListener);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('does not register duplicate online listeners on repeated calls', async () => {
|
|
468
|
+
webex.internal.llm.isConnected = sinon.stub().returns(false);
|
|
469
|
+
|
|
470
|
+
await webinar.updatePSDataChannel();
|
|
471
|
+
await webinar.updatePSDataChannel();
|
|
472
|
+
await webinar.updatePSDataChannel();
|
|
473
|
+
|
|
474
|
+
// Only one 'online' listener should have been registered
|
|
475
|
+
const onlineCalls = webex.internal.llm.on.args.filter(([event]) => event === 'online');
|
|
476
|
+
assert.equal(onlineCalls.length, 1, 'should register exactly one online listener');
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('re-invokes updatePSDataChannel when default session comes online', async () => {
|
|
480
|
+
// Default session is not connected initially
|
|
481
|
+
webex.internal.llm.isConnected = sinon.stub().returns(false);
|
|
482
|
+
|
|
483
|
+
const updatePSDataChannelSpy = sinon.spy(webinar, 'updatePSDataChannel');
|
|
484
|
+
|
|
485
|
+
// First call defers
|
|
486
|
+
await webinar.updatePSDataChannel();
|
|
487
|
+
|
|
488
|
+
// Capture the 'online' listener
|
|
489
|
+
const onlineCall = webex.internal.llm.on.args.find(([event]) => event === 'online');
|
|
490
|
+
assert.isDefined(onlineCall, 'should have registered an online listener');
|
|
491
|
+
|
|
492
|
+
// Now simulate default session coming online
|
|
493
|
+
webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
|
|
494
|
+
return sessionId !== LLM_PRACTICE_SESSION;
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// Fire the captured listener
|
|
498
|
+
onlineCall[1]();
|
|
499
|
+
|
|
500
|
+
// The listener should have cleared itself, removed itself, and re-called updatePSDataChannel
|
|
501
|
+
assert.isNull(webinar._pendingOnlineListener);
|
|
502
|
+
assert.calledWith(webex.internal.llm.off, 'online', sinon.match.func);
|
|
503
|
+
assert.equal(updatePSDataChannelSpy.callCount, 2);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('does not reconnect with stale data if demoted before default session comes online', async () => {
|
|
507
|
+
// Default session is not connected initially
|
|
508
|
+
webex.internal.llm.isConnected = sinon.stub().returns(false);
|
|
509
|
+
|
|
510
|
+
await webinar.updatePSDataChannel();
|
|
511
|
+
|
|
512
|
+
// Capture the 'online' listener
|
|
513
|
+
const onlineCall = webex.internal.llm.on.args.find(([event]) => event === 'online');
|
|
514
|
+
assert.isDefined(onlineCall);
|
|
515
|
+
|
|
516
|
+
// Simulate demotion while waiting
|
|
517
|
+
webinar.selfIsPanelist = false;
|
|
518
|
+
|
|
519
|
+
// Now default session comes online
|
|
520
|
+
webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
|
|
521
|
+
return sessionId !== LLM_PRACTICE_SESSION;
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Fire the listener — re-invokes updatePSDataChannel which will see isPracticeSession = false
|
|
525
|
+
onlineCall[1]();
|
|
526
|
+
|
|
527
|
+
// Should NOT have called registerAndConnect since the user is no longer eligible
|
|
528
|
+
assert.notCalled(webex.internal.llm.registerAndConnect);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('proceeds immediately when default session is already connected', async () => {
|
|
532
|
+
// Default session already connected, practice session not
|
|
533
|
+
webex.internal.llm.isConnected = sinon.stub().callsFake((sessionId) => {
|
|
534
|
+
return sessionId !== LLM_PRACTICE_SESSION;
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
const result = await webinar.updatePSDataChannel();
|
|
538
|
+
|
|
539
|
+
// The 'online' listener is registered then immediately removed since default session is already connected
|
|
540
|
+
assert.calledWith(webex.internal.llm.on, 'online', sinon.match.func);
|
|
541
|
+
assert.calledWith(webex.internal.llm.off, 'online', sinon.match.func);
|
|
542
|
+
assert.isNull(webinar._pendingOnlineListener);
|
|
543
|
+
assert.calledOnce(webex.internal.llm.registerAndConnect);
|
|
544
|
+
assert.equal(result, 'REGISTER_AND_CONNECT_RESULT');
|
|
545
|
+
});
|
|
150
546
|
});
|
|
151
547
|
|
|
152
548
|
describe('#updateStatusByRole', () => {
|
|
153
|
-
let updateLLMConnection;
|
|
154
549
|
let updateMediaShares;
|
|
155
550
|
beforeEach(() => {
|
|
156
|
-
// @ts-ignore
|
|
157
|
-
updateLLMConnection = sinon.stub();
|
|
158
551
|
updateMediaShares = sinon.stub()
|
|
159
552
|
webinar.webex.meetings = {
|
|
160
553
|
getMeetingByType: sinon.stub().returns({
|
|
161
554
|
id: 'meeting-id',
|
|
162
|
-
|
|
163
|
-
|
|
555
|
+
isJoined: sinon.stub().returns(false),
|
|
556
|
+
updateLLMConnection: sinon.stub(),
|
|
557
|
+
shareStatus: SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE,
|
|
164
558
|
locusInfo: {
|
|
165
559
|
mediaShares: 'mediaShares',
|
|
166
560
|
updateMediaShares: updateMediaShares
|
|
@@ -173,40 +567,20 @@ describe('plugin-meetings', () => {
|
|
|
173
567
|
sinon.restore();
|
|
174
568
|
});
|
|
175
569
|
|
|
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
570
|
it('trigger updateMediaShares if promoted', () => {
|
|
197
571
|
|
|
198
572
|
const roleChange = {isPromoted: true, isDemoted: false};
|
|
199
573
|
|
|
200
|
-
|
|
574
|
+
webinar.updateStatusByRole(roleChange);
|
|
201
575
|
|
|
202
|
-
assert.
|
|
576
|
+
assert.calledOnceWithExactly(updateMediaShares, 'mediaShares', true);
|
|
203
577
|
});
|
|
204
578
|
|
|
205
579
|
it('Not trigger updateMediaShares if no role change', () => {
|
|
206
580
|
|
|
207
581
|
const roleChange = {isPromoted: false, isDemoted: false};
|
|
208
582
|
|
|
209
|
-
|
|
583
|
+
webinar.updateStatusByRole(roleChange);
|
|
210
584
|
|
|
211
585
|
assert.notCalled(updateMediaShares);
|
|
212
586
|
});
|
|
@@ -214,18 +588,18 @@ describe('plugin-meetings', () => {
|
|
|
214
588
|
|
|
215
589
|
const roleChange = {isPromoted: true, isDemoted: false};
|
|
216
590
|
|
|
217
|
-
|
|
591
|
+
webinar.updateStatusByRole(roleChange);
|
|
218
592
|
|
|
219
|
-
assert.
|
|
593
|
+
assert.calledOnceWithExactly(updateMediaShares, 'mediaShares', true);
|
|
220
594
|
});
|
|
221
595
|
|
|
222
596
|
it('trigger updateMediaShares if is attendee with whiteboard share', () => {
|
|
223
597
|
|
|
224
598
|
const roleChange = {isPromoted: false, isDemoted: true};
|
|
225
599
|
|
|
226
|
-
|
|
600
|
+
webinar.updateStatusByRole(roleChange);
|
|
227
601
|
|
|
228
|
-
assert.
|
|
602
|
+
assert.calledOnceWithExactly(updateMediaShares, 'mediaShares', true);
|
|
229
603
|
});
|
|
230
604
|
|
|
231
605
|
it('Not trigger updateMediaShares if is attendee with screen share', () => {
|
|
@@ -233,8 +607,9 @@ describe('plugin-meetings', () => {
|
|
|
233
607
|
webinar.webex.meetings = {
|
|
234
608
|
getMeetingByType: sinon.stub().returns({
|
|
235
609
|
id: 'meeting-id',
|
|
236
|
-
|
|
237
|
-
|
|
610
|
+
isJoined: sinon.stub().returns(false),
|
|
611
|
+
updateLLMConnection: sinon.stub(),
|
|
612
|
+
shareStatus: SHARE_STATUS.REMOTE_SHARE_ACTIVE,
|
|
238
613
|
locusInfo: {
|
|
239
614
|
mediaShares: 'mediaShares',
|
|
240
615
|
updateMediaShares: updateMediaShares
|
|
@@ -244,10 +619,18 @@ describe('plugin-meetings', () => {
|
|
|
244
619
|
|
|
245
620
|
const roleChange = {isPromoted: false, isDemoted: true};
|
|
246
621
|
|
|
247
|
-
|
|
622
|
+
webinar.updateStatusByRole(roleChange);
|
|
248
623
|
|
|
249
624
|
assert.notCalled(updateMediaShares);
|
|
250
625
|
});
|
|
626
|
+
|
|
627
|
+
it('updates PS data channel based on join eligibility', () => {
|
|
628
|
+
const updatePSDataChannelStub = sinon.stub(webinar, 'updatePSDataChannel').resolves();
|
|
629
|
+
|
|
630
|
+
webinar.updateStatusByRole({isPromoted: false, isDemoted: false});
|
|
631
|
+
|
|
632
|
+
assert.calledOnceWithExactly(updatePSDataChannelStub);
|
|
633
|
+
});
|
|
251
634
|
});
|
|
252
635
|
|
|
253
636
|
describe("#setPracticeSessionState", () => {
|
|
@@ -323,6 +706,14 @@ describe('plugin-meetings', () => {
|
|
|
323
706
|
|
|
324
707
|
assert.equal(webinar.practiceSessionEnabled, false);
|
|
325
708
|
});
|
|
709
|
+
it('triggers PS data channel update using computed eligibility', () => {
|
|
710
|
+
webinar.selfIsPanelist = true;
|
|
711
|
+
const updatePSDataChannelStub = sinon.stub(webinar, 'updatePSDataChannel').resolves();
|
|
712
|
+
|
|
713
|
+
webinar.updatePracticeSessionStatus({enabled: true});
|
|
714
|
+
|
|
715
|
+
assert.calledOnceWithExactly(updatePSDataChannelStub);
|
|
716
|
+
});
|
|
326
717
|
});
|
|
327
718
|
|
|
328
719
|
describe("#startWebcast", () => {
|
|
@@ -631,5 +1022,111 @@ describe('plugin-meetings', () => {
|
|
|
631
1022
|
}
|
|
632
1023
|
});
|
|
633
1024
|
});
|
|
1025
|
+
|
|
1026
|
+
describe("#searchLargeScaleWebinarAttendees", () => {
|
|
1027
|
+
const attendeeSearchUrl = 'https://locusUrl/attendees/search';
|
|
1028
|
+
const params = {
|
|
1029
|
+
queryString: 'queryString',
|
|
1030
|
+
limit: 50,
|
|
1031
|
+
next: null,
|
|
1032
|
+
};
|
|
1033
|
+
beforeEach(() => {
|
|
1034
|
+
// @ts-ignore
|
|
1035
|
+
webinar.webex.meetings = {
|
|
1036
|
+
getMeetingByType: sinon.stub().returns({
|
|
1037
|
+
id: 'meeting-id',
|
|
1038
|
+
locusInfo: {
|
|
1039
|
+
links:{
|
|
1040
|
+
resources: {
|
|
1041
|
+
attendeeSearch: {
|
|
1042
|
+
url: attendeeSearchUrl
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
})
|
|
1048
|
+
};
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
it('throws an error if attendeeSearchUrl is not available', async () => {
|
|
1052
|
+
webinar.webex.meetings = {
|
|
1053
|
+
getMeetingByType: sinon.stub().returns({
|
|
1054
|
+
id: 'meeting-id',
|
|
1055
|
+
locusInfo: {
|
|
1056
|
+
links:{
|
|
1057
|
+
resources: {
|
|
1058
|
+
attendeeSearch: {
|
|
1059
|
+
url: null
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
})
|
|
1065
|
+
};
|
|
1066
|
+
try {
|
|
1067
|
+
await webinar.searchLargeScaleWebinarAttendees(params);
|
|
1068
|
+
assert.fail('searchLargeScaleWebinarAttendees should throw an error');
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
assert.equal(error.message,'Meeting:webinar5k#Attendee search url is not available', 'should throw the correct error');
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
it('sends a GET request to search the large scale webinar attendees', async () => {
|
|
1075
|
+
const result = await webinar.searchLargeScaleWebinarAttendees(params);
|
|
1076
|
+
assert.calledOnce(webex.request);
|
|
1077
|
+
assert.calledWith(webex.request, {
|
|
1078
|
+
method: 'GET',
|
|
1079
|
+
uri: `${attendeeSearchUrl}?search_text=${encodeURIComponent(params.queryString)}&limit=50`,
|
|
1080
|
+
headers: {
|
|
1081
|
+
authorization: 'test-token',
|
|
1082
|
+
trackingId: 'webex-js-sdk_test-uuid',
|
|
1083
|
+
},
|
|
1084
|
+
});
|
|
1085
|
+
assert.equal(
|
|
1086
|
+
result,
|
|
1087
|
+
'REQUEST_RETURN_VALUE',
|
|
1088
|
+
'should return the resolved value from the request'
|
|
1089
|
+
);
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
it('queryString is empty string', async () => {
|
|
1093
|
+
params.queryString = '';
|
|
1094
|
+
const result = await webinar.searchLargeScaleWebinarAttendees(params);
|
|
1095
|
+
assert.calledOnce(webex.request);
|
|
1096
|
+
assert.calledWith(webex.request, {
|
|
1097
|
+
method: 'GET',
|
|
1098
|
+
uri: `${attendeeSearchUrl}?limit=50`,
|
|
1099
|
+
headers: {
|
|
1100
|
+
authorization: 'test-token',
|
|
1101
|
+
trackingId: 'webex-js-sdk_test-uuid',
|
|
1102
|
+
},
|
|
1103
|
+
});
|
|
1104
|
+
assert.equal(
|
|
1105
|
+
result,
|
|
1106
|
+
'REQUEST_RETURN_VALUE',
|
|
1107
|
+
'should return the resolved value from the request'
|
|
1108
|
+
);
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
it('handles API call failures gracefully', async () => {
|
|
1112
|
+
webex.request.rejects(new Error('API_ERROR'));
|
|
1113
|
+
const errorLogger = sinon.stub(LoggerProxy.logger, 'error');
|
|
1114
|
+
|
|
1115
|
+
try {
|
|
1116
|
+
await webinar.searchLargeScaleWebinarAttendees(params);
|
|
1117
|
+
assert.fail('searchLargeScaleWebinarAttendees should throw an error');
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
assert.equal(error.message, 'API_ERROR', 'should throw the correct error');
|
|
1120
|
+
assert.calledOnce(errorLogger);
|
|
1121
|
+
assert.calledWith(
|
|
1122
|
+
errorLogger,
|
|
1123
|
+
'Meeting:webinar5k#searchLargeScaleWebinarAttendees failed',
|
|
1124
|
+
sinon.match.instanceOf(Error)
|
|
1125
|
+
);
|
|
1126
|
+
} finally {
|
|
1127
|
+
errorLogger.restore();
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
634
1131
|
})
|
|
635
1132
|
})
|
|
@@ -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
|
+
});
|