@webex/plugin-meetings 3.12.0-next.6 → 3.12.0-next.61
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/AGENTS.md +9 -0
- package/dist/aiEnableRequest/index.js +15 -2
- package/dist/aiEnableRequest/index.js.map +1 -1
- package/dist/breakouts/breakout.js +8 -3
- package/dist/breakouts/breakout.js.map +1 -1
- package/dist/breakouts/index.js +26 -2
- package/dist/breakouts/index.js.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +6 -3
- package/dist/constants.js.map +1 -1
- package/dist/controls-options-manager/constants.js +11 -1
- package/dist/controls-options-manager/constants.js.map +1 -1
- package/dist/controls-options-manager/index.js +38 -24
- package/dist/controls-options-manager/index.js.map +1 -1
- package/dist/controls-options-manager/util.js +91 -0
- package/dist/controls-options-manager/util.js.map +1 -1
- package/dist/hashTree/constants.js +10 -1
- package/dist/hashTree/constants.js.map +1 -1
- package/dist/hashTree/hashTreeParser.js +716 -370
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/hashTree/utils.js +22 -0
- package/dist/hashTree/utils.js.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptors/locusRetry.js +23 -8
- package/dist/interceptors/locusRetry.js.map +1 -1
- package/dist/interpretation/index.js +10 -1
- package/dist/interpretation/index.js.map +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/controlsUtils.js +4 -1
- package/dist/locus-info/controlsUtils.js.map +1 -1
- package/dist/locus-info/index.js +289 -87
- package/dist/locus-info/index.js.map +1 -1
- package/dist/locus-info/types.js +19 -0
- package/dist/locus-info/types.js.map +1 -1
- package/dist/media/index.js +3 -1
- package/dist/media/index.js.map +1 -1
- package/dist/media/properties.js +1 -0
- package/dist/media/properties.js.map +1 -1
- package/dist/meeting/in-meeting-actions.js +3 -1
- package/dist/meeting/in-meeting-actions.js.map +1 -1
- package/dist/meeting/index.js +907 -535
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/util.js +19 -2
- package/dist/meeting/util.js.map +1 -1
- package/dist/meetings/index.js +231 -78
- package/dist/meetings/index.js.map +1 -1
- package/dist/meetings/meetings.types.js +6 -1
- package/dist/meetings/meetings.types.js.map +1 -1
- package/dist/meetings/request.js +39 -0
- package/dist/meetings/request.js.map +1 -1
- package/dist/meetings/util.js +79 -5
- package/dist/meetings/util.js.map +1 -1
- package/dist/member/index.js +10 -0
- package/dist/member/index.js.map +1 -1
- package/dist/member/types.js.map +1 -1
- package/dist/member/util.js +3 -0
- package/dist/member/util.js.map +1 -1
- package/dist/metrics/constants.js +4 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/multistream/codec/constants.js +63 -0
- package/dist/multistream/codec/constants.js.map +1 -0
- package/dist/multistream/mediaRequestManager.js +62 -15
- package/dist/multistream/mediaRequestManager.js.map +1 -1
- package/dist/multistream/receiveSlot.js +9 -0
- package/dist/multistream/receiveSlot.js.map +1 -1
- package/dist/reactions/reactions.type.js.map +1 -1
- package/dist/recording-controller/index.js +1 -3
- package/dist/recording-controller/index.js.map +1 -1
- package/dist/types/config.d.ts +2 -0
- package/dist/types/constants.d.ts +2 -0
- package/dist/types/controls-options-manager/constants.d.ts +6 -1
- package/dist/types/controls-options-manager/index.d.ts +10 -0
- package/dist/types/hashTree/constants.d.ts +1 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +92 -16
- package/dist/types/hashTree/utils.d.ts +11 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/interceptors/locusRetry.d.ts +4 -4
- package/dist/types/locus-info/index.d.ts +46 -6
- package/dist/types/locus-info/types.d.ts +21 -1
- package/dist/types/media/properties.d.ts +1 -0
- package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
- package/dist/types/meeting/index.d.ts +87 -3
- package/dist/types/meeting/util.d.ts +8 -0
- package/dist/types/meetings/index.d.ts +30 -2
- package/dist/types/meetings/meetings.types.d.ts +15 -0
- package/dist/types/meetings/request.d.ts +14 -0
- package/dist/types/member/index.d.ts +1 -0
- package/dist/types/member/types.d.ts +1 -0
- package/dist/types/member/util.d.ts +1 -0
- package/dist/types/metrics/constants.d.ts +3 -0
- package/dist/types/multistream/codec/constants.d.ts +7 -0
- package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
- package/dist/types/reactions/reactions.type.d.ts +3 -0
- package/dist/webinar/index.js +361 -235
- package/dist/webinar/index.js.map +1 -1
- package/package.json +22 -22
- package/src/aiEnableRequest/index.ts +16 -0
- package/src/breakouts/breakout.ts +3 -1
- package/src/breakouts/index.ts +31 -0
- package/src/config.ts +2 -0
- package/src/constants.ts +5 -1
- package/src/controls-options-manager/constants.ts +14 -1
- package/src/controls-options-manager/index.ts +47 -24
- package/src/controls-options-manager/util.ts +81 -1
- package/src/hashTree/constants.ts +9 -0
- package/src/hashTree/hashTreeParser.ts +429 -183
- package/src/hashTree/utils.ts +17 -0
- package/src/index.ts +5 -0
- package/src/interceptors/locusRetry.ts +25 -4
- package/src/interpretation/index.ts +25 -8
- package/src/locus-info/controlsUtils.ts +3 -1
- package/src/locus-info/index.ts +291 -97
- package/src/locus-info/types.ts +25 -1
- package/src/media/index.ts +3 -0
- package/src/media/properties.ts +1 -0
- package/src/meeting/in-meeting-actions.ts +4 -0
- package/src/meeting/index.ts +388 -33
- package/src/meeting/util.ts +20 -2
- package/src/meetings/index.ts +134 -44
- package/src/meetings/meetings.types.ts +19 -0
- package/src/meetings/request.ts +43 -0
- package/src/meetings/util.ts +97 -1
- package/src/member/index.ts +10 -0
- package/src/member/types.ts +1 -0
- package/src/member/util.ts +3 -0
- package/src/metrics/constants.ts +3 -0
- package/src/multistream/codec/constants.ts +58 -0
- package/src/multistream/mediaRequestManager.ts +119 -28
- package/src/multistream/receiveSlot.ts +18 -0
- package/src/reactions/reactions.type.ts +3 -0
- package/src/recording-controller/index.ts +1 -2
- package/src/webinar/index.ts +162 -21
- package/test/unit/spec/aiEnableRequest/index.ts +86 -0
- package/test/unit/spec/breakouts/breakout.ts +9 -3
- package/test/unit/spec/breakouts/index.ts +49 -0
- package/test/unit/spec/controls-options-manager/index.js +140 -29
- package/test/unit/spec/controls-options-manager/util.js +165 -0
- package/test/unit/spec/hashTree/hashTreeParser.ts +1508 -149
- package/test/unit/spec/hashTree/utils.ts +88 -1
- package/test/unit/spec/interceptors/locusRetry.ts +205 -4
- package/test/unit/spec/interpretation/index.ts +26 -4
- package/test/unit/spec/locus-info/controlsUtils.js +172 -57
- package/test/unit/spec/locus-info/index.js +475 -81
- package/test/unit/spec/media/index.ts +31 -0
- package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
- package/test/unit/spec/meeting/index.js +1131 -49
- package/test/unit/spec/meeting/muteState.js +3 -0
- package/test/unit/spec/meeting/utils.js +33 -0
- package/test/unit/spec/meetings/index.js +360 -10
- package/test/unit/spec/meetings/request.js +141 -0
- package/test/unit/spec/meetings/utils.js +189 -0
- package/test/unit/spec/member/index.js +7 -0
- package/test/unit/spec/member/util.js +24 -0
- package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
- package/test/unit/spec/recording-controller/index.js +9 -8
- package/test/unit/spec/webinar/index.ts +141 -16
|
@@ -11,6 +11,7 @@ describe('plugin-meetings', () => {
|
|
|
11
11
|
let audio;
|
|
12
12
|
let video;
|
|
13
13
|
let originalRemoteUpdateAudioVideo;
|
|
14
|
+
let originalUpdateLocusFromApiResponse;
|
|
14
15
|
|
|
15
16
|
const fakeLocusResponse = {body: {locus: {info: 'this is a fake locus'}}};
|
|
16
17
|
|
|
@@ -45,6 +46,7 @@ describe('plugin-meetings', () => {
|
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
originalRemoteUpdateAudioVideo = MeetingUtil.remoteUpdateAudioVideo;
|
|
49
|
+
originalUpdateLocusFromApiResponse = MeetingUtil.updateLocusFromApiResponse;
|
|
48
50
|
|
|
49
51
|
MeetingUtil.remoteUpdateAudioVideo = sinon.stub().resolves(fakeLocusResponse);
|
|
50
52
|
MeetingUtil.updateLocusFromApiResponse = sinon.stub();
|
|
@@ -57,6 +59,7 @@ describe('plugin-meetings', () => {
|
|
|
57
59
|
|
|
58
60
|
afterEach(() => {
|
|
59
61
|
MeetingUtil.remoteUpdateAudioVideo = originalRemoteUpdateAudioVideo;
|
|
62
|
+
MeetingUtil.updateLocusFromApiResponse = originalUpdateLocusFromApiResponse;
|
|
60
63
|
});
|
|
61
64
|
|
|
62
65
|
describe('mute state library', () => {
|
|
@@ -60,6 +60,7 @@ describe('plugin-meetings', () => {
|
|
|
60
60
|
meeting.annotaion = {cleanUp: sinon.stub()};
|
|
61
61
|
meeting.getWebexObject = sinon.stub().returns(webex);
|
|
62
62
|
meeting.simultaneousInterpretation = {cleanUp: sinon.stub()};
|
|
63
|
+
meeting.locusInfo = {cleanUp: sinon.stub()};
|
|
63
64
|
meeting.trigger = sinon.stub();
|
|
64
65
|
meeting.webex = webex;
|
|
65
66
|
meeting.webex.internal.newMetrics.callDiagnosticMetrics =
|
|
@@ -89,6 +90,7 @@ describe('plugin-meetings', () => {
|
|
|
89
90
|
assert.calledOnceWithExactly(meeting.cleanupLLMConneciton, {throwOnError: false});
|
|
90
91
|
assert.calledOnce(meeting.breakouts.cleanUp);
|
|
91
92
|
assert.calledOnce(meeting.simultaneousInterpretation.cleanUp);
|
|
93
|
+
assert.calledOnce(meeting.locusInfo.cleanUp);
|
|
92
94
|
assert.calledOnce(webex.internal.device.meetingEnded);
|
|
93
95
|
assert.calledOnceWithExactly(
|
|
94
96
|
meeting.webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId,
|
|
@@ -110,6 +112,7 @@ describe('plugin-meetings', () => {
|
|
|
110
112
|
assert.notCalled(meeting.cleanupLLMConneciton);
|
|
111
113
|
assert.calledOnce(meeting.breakouts.cleanUp);
|
|
112
114
|
assert.calledOnce(meeting.simultaneousInterpretation.cleanUp);
|
|
115
|
+
assert.calledOnce(meeting.locusInfo.cleanUp);
|
|
113
116
|
assert.calledOnce(webex.internal.device.meetingEnded);
|
|
114
117
|
assert.calledOnceWithExactly(
|
|
115
118
|
meeting.webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId,
|
|
@@ -130,6 +133,7 @@ describe('plugin-meetings', () => {
|
|
|
130
133
|
assert.notCalled(meeting.cleanupLLMConneciton);
|
|
131
134
|
assert.calledOnce(meeting.breakouts.cleanUp);
|
|
132
135
|
assert.calledOnce(meeting.simultaneousInterpretation.cleanUp);
|
|
136
|
+
assert.calledOnce(meeting.locusInfo.cleanUp);
|
|
133
137
|
assert.calledOnce(webex.internal.device.meetingEnded);
|
|
134
138
|
assert.calledOnceWithExactly(
|
|
135
139
|
meeting.webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId,
|
|
@@ -272,6 +276,31 @@ describe('plugin-meetings', () => {
|
|
|
272
276
|
assert.notCalled(meeting.locusInfo.handleLocusAPIResponse);
|
|
273
277
|
});
|
|
274
278
|
|
|
279
|
+
it('should call handleLocusAPIResponse when response body is an unwrapped LocusDTO', () => {
|
|
280
|
+
const meeting = {
|
|
281
|
+
locusInfo: {
|
|
282
|
+
handleLocusAPIResponse: sinon.stub(),
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const originalResponse = {
|
|
287
|
+
body: {
|
|
288
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/some-id',
|
|
289
|
+
participants: [],
|
|
290
|
+
self: {},
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const response = MeetingUtil.updateLocusFromApiResponse(meeting, originalResponse);
|
|
295
|
+
|
|
296
|
+
assert.deepEqual(response, originalResponse);
|
|
297
|
+
assert.calledOnceWithExactly(
|
|
298
|
+
meeting.locusInfo.handleLocusAPIResponse,
|
|
299
|
+
meeting,
|
|
300
|
+
originalResponse.body
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
|
|
275
304
|
it('should work with an undefined meeting', () => {
|
|
276
305
|
const originalResponse = {
|
|
277
306
|
body: {
|
|
@@ -1146,6 +1175,10 @@ describe('plugin-meetings', () => {
|
|
|
1146
1175
|
{functionName: 'canSelectSpokenLanguages', displayHint: 'DISPLAY_NON_ENGLISH_ASR'},
|
|
1147
1176
|
{functionName: 'waitingForOthersToJoin', displayHint: 'WAITING_FOR_OTHERS'},
|
|
1148
1177
|
{functionName: 'showAutoEndMeetingWarning', displayHint: 'SHOW_AUTO_END_MEETING_WARNING'},
|
|
1178
|
+
{
|
|
1179
|
+
functionName: 'isAnonymizeDisplayNamesEnabled',
|
|
1180
|
+
displayHint: 'ANONYMOUS_DISPLAY_NAMES_ENABLED',
|
|
1181
|
+
},
|
|
1149
1182
|
].forEach(({functionName, displayHint}) => {
|
|
1150
1183
|
describe(functionName, () => {
|
|
1151
1184
|
it('works as expected', () => {
|
|
@@ -14,12 +14,14 @@ import StaticConfig from '@webex/plugin-meetings/src/common/config';
|
|
|
14
14
|
import TriggerProxy from '@webex/plugin-meetings/src/common/events/trigger-proxy';
|
|
15
15
|
import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
|
|
16
16
|
import LoggerConfig from '@webex/plugin-meetings/src/common/logs/logger-config';
|
|
17
|
+
import ParameterError from '@webex/plugin-meetings/src/common/errors/parameter';
|
|
17
18
|
import Meeting, {CallStateForMetrics} from '@webex/plugin-meetings/src/meeting';
|
|
18
19
|
import {Services} from '@webex/webex-core';
|
|
19
20
|
import MeetingUtil from '@webex/plugin-meetings/src/meeting/util';
|
|
20
21
|
import Meetings from '@webex/plugin-meetings/src/meetings';
|
|
21
22
|
import MeetingCollection from '@webex/plugin-meetings/src/meetings/collection';
|
|
22
23
|
import MeetingsUtil from '@webex/plugin-meetings/src/meetings/util';
|
|
24
|
+
import {SitePreferenceSelectOption} from '@webex/plugin-meetings/src/meetings/meetings.types';
|
|
23
25
|
import PersonalMeetingRoom from '@webex/plugin-meetings/src/personal-meeting-room';
|
|
24
26
|
import Reachability from '@webex/plugin-meetings/src/reachability';
|
|
25
27
|
import Metrics from '@webex/plugin-meetings/src/metrics';
|
|
@@ -91,6 +93,7 @@ describe('plugin-meetings', () => {
|
|
|
91
93
|
locusInfo = {
|
|
92
94
|
parse: sinon.stub().returns(true),
|
|
93
95
|
updateMainSessionLocusCache: sinon.stub(),
|
|
96
|
+
syncAllHashTreeDatasets: sinon.stub(),
|
|
94
97
|
};
|
|
95
98
|
webex = new MockWebex({
|
|
96
99
|
children: {
|
|
@@ -423,6 +426,33 @@ describe('plugin-meetings', () => {
|
|
|
423
426
|
});
|
|
424
427
|
});
|
|
425
428
|
|
|
429
|
+
describe('#_toggleEnableAv1SlidesSupport', () => {
|
|
430
|
+
it('should have _toggleEnableAv1SlidesSupport', () => {
|
|
431
|
+
assert.equal(typeof webex.meetings._toggleEnableAv1SlidesSupport, 'function');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe('success', () => {
|
|
435
|
+
it('should update meetings config to enable AV1 slides support', () => {
|
|
436
|
+
webex.meetings._toggleEnableAv1SlidesSupport(true);
|
|
437
|
+
assert.equal(webex.meetings.config.enableAv1SlidesSupport, true);
|
|
438
|
+
|
|
439
|
+
webex.meetings._toggleEnableAv1SlidesSupport(false);
|
|
440
|
+
assert.equal(webex.meetings.config.enableAv1SlidesSupport, false);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('should not update config when called with a non-boolean value', () => {
|
|
444
|
+
webex.meetings._toggleEnableAv1SlidesSupport(true);
|
|
445
|
+
assert.equal(webex.meetings.config.enableAv1SlidesSupport, true);
|
|
446
|
+
|
|
447
|
+
webex.meetings._toggleEnableAv1SlidesSupport('invalid');
|
|
448
|
+
assert.equal(webex.meetings.config.enableAv1SlidesSupport, true);
|
|
449
|
+
|
|
450
|
+
webex.meetings._toggleEnableAv1SlidesSupport(undefined);
|
|
451
|
+
assert.equal(webex.meetings.config.enableAv1SlidesSupport, true);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
426
456
|
describe('#_toggleStopIceGatheringAfterFirstRelayCandidate', () => {
|
|
427
457
|
it('should have _toggleStopIceGatheringAfterFirstRelayCandidate', () => {
|
|
428
458
|
assert.equal(
|
|
@@ -1355,6 +1385,87 @@ describe('plugin-meetings', () => {
|
|
|
1355
1385
|
);
|
|
1356
1386
|
});
|
|
1357
1387
|
});
|
|
1388
|
+
describe('#fetchSitePreferencesMeViaSite', () => {
|
|
1389
|
+
const sitePreferencesResponse = {
|
|
1390
|
+
scheduling: {
|
|
1391
|
+
supportScheduleWebinar: true,
|
|
1392
|
+
webinarWebLink: 'https://go.webex.com/webappng/sites/go/webinar/scheduler',
|
|
1393
|
+
},
|
|
1394
|
+
};
|
|
1395
|
+
|
|
1396
|
+
beforeEach(() => {
|
|
1397
|
+
webex.meetings.request.fetchSitePreferencesMeViaSite = sinon
|
|
1398
|
+
.stub()
|
|
1399
|
+
.resolves(sitePreferencesResponse);
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
it('should have #fetchSitePreferencesMeViaSite', () => {
|
|
1403
|
+
assert.exists(webex.meetings.fetchSitePreferencesMeViaSite);
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
it('fetches scheduling preferences for the preferred Webex site by default', async () => {
|
|
1407
|
+
webex.meetings.preferredWebexSite = 'go.webex.com';
|
|
1408
|
+
|
|
1409
|
+
const result = await webex.meetings.fetchSitePreferencesMeViaSite();
|
|
1410
|
+
|
|
1411
|
+
assert.deepEqual(result, sitePreferencesResponse);
|
|
1412
|
+
assert.calledOnceWithExactly(
|
|
1413
|
+
webex.meetings.request.fetchSitePreferencesMeViaSite,
|
|
1414
|
+
{
|
|
1415
|
+
siteUrl: 'go.webex.com',
|
|
1416
|
+
}
|
|
1417
|
+
);
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
it('uses the provided Webex site instead of the preferred Webex site', async () => {
|
|
1421
|
+
webex.meetings.preferredWebexSite = 'preferred.webex.com';
|
|
1422
|
+
|
|
1423
|
+
await webex.meetings.fetchSitePreferencesMeViaSite({siteUrl: 'go.webex.com'});
|
|
1424
|
+
|
|
1425
|
+
assert.calledOnceWithExactly(
|
|
1426
|
+
webex.meetings.request.fetchSitePreferencesMeViaSite,
|
|
1427
|
+
{
|
|
1428
|
+
siteUrl: 'go.webex.com',
|
|
1429
|
+
}
|
|
1430
|
+
);
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
it('forwards custom site name and preference sections to the request helper', async () => {
|
|
1434
|
+
webex.meetings.preferredWebexSite = 'go.webex.com';
|
|
1435
|
+
|
|
1436
|
+
await webex.meetings.fetchSitePreferencesMeViaSite({
|
|
1437
|
+
siteName: 'custom-site',
|
|
1438
|
+
selectOptions: [SitePreferenceSelectOption.SCHEDULING],
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
assert.calledOnceWithExactly(
|
|
1442
|
+
webex.meetings.request.fetchSitePreferencesMeViaSite,
|
|
1443
|
+
{
|
|
1444
|
+
siteUrl: 'go.webex.com',
|
|
1445
|
+
siteName: 'custom-site',
|
|
1446
|
+
selectOptions: [SitePreferenceSelectOption.SCHEDULING],
|
|
1447
|
+
}
|
|
1448
|
+
);
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
it('throws when no Webex site is available', () => {
|
|
1452
|
+
webex.meetings.preferredWebexSite = '';
|
|
1453
|
+
webex.meetings.request.fetchSitePreferencesMeViaSite.throws(
|
|
1454
|
+
new ParameterError(
|
|
1455
|
+
'No siteUrl available. Call register() before fetching site preferences or provide options.siteUrl.'
|
|
1456
|
+
)
|
|
1457
|
+
);
|
|
1458
|
+
|
|
1459
|
+
assert.throws(
|
|
1460
|
+
() => webex.meetings.fetchSitePreferencesMeViaSite(),
|
|
1461
|
+
ParameterError,
|
|
1462
|
+
'No siteUrl available. Call register() before fetching site preferences or provide options.siteUrl.'
|
|
1463
|
+
);
|
|
1464
|
+
assert.calledOnceWithExactly(webex.meetings.request.fetchSitePreferencesMeViaSite, {
|
|
1465
|
+
siteUrl: '',
|
|
1466
|
+
});
|
|
1467
|
+
});
|
|
1468
|
+
});
|
|
1358
1469
|
describe('Static shortcut proxy methods', () => {
|
|
1359
1470
|
describe('MeetingCollection getByKey proxies', () => {
|
|
1360
1471
|
beforeEach(() => {
|
|
@@ -1391,7 +1502,7 @@ describe('plugin-meetings', () => {
|
|
|
1391
1502
|
it('should have #syncMeetings', () => {
|
|
1392
1503
|
assert.exists(webex.meetings.syncMeetings);
|
|
1393
1504
|
});
|
|
1394
|
-
it('should
|
|
1505
|
+
it('should skip getActiveMeetings but still call syncAllHashTreeDatasets if unverified guest', async () => {
|
|
1395
1506
|
webex.meetings.request.getActiveMeetings = sinon.stub().returns(
|
|
1396
1507
|
Promise.resolve({
|
|
1397
1508
|
loci: [
|
|
@@ -1404,13 +1515,23 @@ describe('plugin-meetings', () => {
|
|
|
1404
1515
|
webex.credentials.isUnverifiedGuest = true;
|
|
1405
1516
|
LoggerProxy.logger.info = sinon.stub();
|
|
1406
1517
|
|
|
1518
|
+
const mockLocusInfo = {
|
|
1519
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
1520
|
+
};
|
|
1521
|
+
webex.meetings.meetingCollection.getAll = sinon.stub().returns({
|
|
1522
|
+
meeting1: {locusInfo: mockLocusInfo},
|
|
1523
|
+
meeting2: {locusInfo: undefined},
|
|
1524
|
+
meeting3: {},
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1407
1527
|
await webex.meetings.syncMeetings();
|
|
1408
1528
|
|
|
1409
1529
|
assert.notCalled(webex.meetings.request.getActiveMeetings);
|
|
1410
1530
|
assert.calledWith(
|
|
1411
1531
|
LoggerProxy.logger.info,
|
|
1412
|
-
'Meetings:index#syncMeetings --> skipping
|
|
1532
|
+
'Meetings:index#syncMeetings --> user is unverified guest, skipping calling Locus for meeting sync'
|
|
1413
1533
|
);
|
|
1534
|
+
assert.calledOnce(mockLocusInfo.syncAllHashTreeDatasets);
|
|
1414
1535
|
});
|
|
1415
1536
|
describe('succesful requests', () => {
|
|
1416
1537
|
beforeEach(() => {
|
|
@@ -1429,6 +1550,9 @@ describe('plugin-meetings', () => {
|
|
|
1429
1550
|
webex.meetings.meetingCollection.getByKey = sinon.stub().returns({
|
|
1430
1551
|
locusInfo,
|
|
1431
1552
|
});
|
|
1553
|
+
webex.meetings.meetingCollection.getAll = sinon.stub().returns({
|
|
1554
|
+
meeting1: {locusInfo, locusUrl: url1},
|
|
1555
|
+
});
|
|
1432
1556
|
});
|
|
1433
1557
|
it('tests the sync meeting calls for existing meeting', async () => {
|
|
1434
1558
|
await webex.meetings.syncMeetings();
|
|
@@ -1436,6 +1560,7 @@ describe('plugin-meetings', () => {
|
|
|
1436
1560
|
assert.calledOnce(webex.meetings.meetingCollection.getByKey);
|
|
1437
1561
|
assert.calledOnce(locusInfo.parse);
|
|
1438
1562
|
assert.calledWith(webex.meetings.meetingCollection.getByKey, 'locusUrl', url1);
|
|
1563
|
+
assert.calledOnce(locusInfo.syncAllHashTreeDatasets);
|
|
1439
1564
|
});
|
|
1440
1565
|
});
|
|
1441
1566
|
describe('when meeting is not returned', () => {
|
|
@@ -1474,7 +1599,7 @@ describe('plugin-meetings', () => {
|
|
|
1474
1599
|
url: url1,
|
|
1475
1600
|
},
|
|
1476
1601
|
hashTreeMessage: undefined,
|
|
1477
|
-
});
|
|
1602
|
+
}, sinon.match.func);
|
|
1478
1603
|
});
|
|
1479
1604
|
});
|
|
1480
1605
|
describe('when destroying meeting is needed', () => {
|
|
@@ -1520,7 +1645,7 @@ describe('plugin-meetings', () => {
|
|
|
1520
1645
|
it('destroy any meeting that has no active locus url if keepOnlyLocusMeetings is not defined', async () => {
|
|
1521
1646
|
await webex.meetings.syncMeetings();
|
|
1522
1647
|
assert.calledOnce(webex.meetings.request.getActiveMeetings);
|
|
1523
|
-
assert.
|
|
1648
|
+
assert.calledTwice(webex.meetings.meetingCollection.getAll);
|
|
1524
1649
|
assert.calledWith(destroySpy, meetingCollectionMeetings.noLongerValidLocusMeeting);
|
|
1525
1650
|
assert.calledWith(destroySpy, meetingCollectionMeetings.otherNonLocusMeeting1);
|
|
1526
1651
|
assert.calledWith(destroySpy, meetingCollectionMeetings.otherNonLocusMeeting2);
|
|
@@ -1532,7 +1657,7 @@ describe('plugin-meetings', () => {
|
|
|
1532
1657
|
it('destroy any meeting that has no active locus url if keepOnlyLocusMeetings === true', async () => {
|
|
1533
1658
|
await webex.meetings.syncMeetings({keepOnlyLocusMeetings: true});
|
|
1534
1659
|
assert.calledOnce(webex.meetings.request.getActiveMeetings);
|
|
1535
|
-
assert.
|
|
1660
|
+
assert.calledTwice(webex.meetings.meetingCollection.getAll);
|
|
1536
1661
|
assert.calledWith(destroySpy, meetingCollectionMeetings.noLongerValidLocusMeeting);
|
|
1537
1662
|
assert.calledWith(destroySpy, meetingCollectionMeetings.otherNonLocusMeeting1);
|
|
1538
1663
|
assert.calledWith(destroySpy, meetingCollectionMeetings.otherNonLocusMeeting2);
|
|
@@ -1544,7 +1669,7 @@ describe('plugin-meetings', () => {
|
|
|
1544
1669
|
it('destroy any LOCUS meetings that have no active locus url if keepOnlyLocusMeetings === false', async () => {
|
|
1545
1670
|
await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false});
|
|
1546
1671
|
assert.calledOnce(webex.meetings.request.getActiveMeetings);
|
|
1547
|
-
assert.
|
|
1672
|
+
assert.calledTwice(webex.meetings.meetingCollection.getAll);
|
|
1548
1673
|
assert.calledWith(destroySpy, meetingCollectionMeetings.noLongerValidLocusMeeting);
|
|
1549
1674
|
assert.callCount(destroySpy, 1);
|
|
1550
1675
|
|
|
@@ -1552,6 +1677,147 @@ describe('plugin-meetings', () => {
|
|
|
1552
1677
|
});
|
|
1553
1678
|
});
|
|
1554
1679
|
});
|
|
1680
|
+
|
|
1681
|
+
describe('when globalMeetingId preserves breakout meetings', () => {
|
|
1682
|
+
let destroySpy;
|
|
1683
|
+
let cleanUpSpy;
|
|
1684
|
+
|
|
1685
|
+
beforeEach(() => {
|
|
1686
|
+
destroySpy = sinon.spy(webex.meetings, 'destroy');
|
|
1687
|
+
cleanUpSpy = sinon.stub(MeetingUtil, 'cleanUp').returns(Promise.resolve());
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
afterEach(() => {
|
|
1691
|
+
cleanUpSpy.restore();
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
it('should not destroy a meeting whose globalMeetingId matches an active locus', async () => {
|
|
1695
|
+
const meetingCollectionMeetings = {
|
|
1696
|
+
breakoutMeeting: {
|
|
1697
|
+
locusUrl: 'breakout-url',
|
|
1698
|
+
locusInfo: {
|
|
1699
|
+
info: {globalMeetingId: 'gmid-123'},
|
|
1700
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
1701
|
+
},
|
|
1702
|
+
sendCallAnalyzerMetrics: sinon.stub(),
|
|
1703
|
+
},
|
|
1704
|
+
};
|
|
1705
|
+
|
|
1706
|
+
webex.meetings.meetingCollection.getAll = sinon
|
|
1707
|
+
.stub()
|
|
1708
|
+
.returns(meetingCollectionMeetings);
|
|
1709
|
+
webex.meetings.request.getActiveMeetings = sinon.stub().resolves({
|
|
1710
|
+
loci: [{url: 'main-url', info: {globalMeetingId: 'gmid-123'}}],
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1713
|
+
await webex.meetings.syncMeetings();
|
|
1714
|
+
|
|
1715
|
+
assert.notCalled(destroySpy);
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
it('should destroy a meeting whose globalMeetingId does NOT match any active locus', async () => {
|
|
1719
|
+
const meetingCollectionMeetings = {
|
|
1720
|
+
breakoutMeeting: {
|
|
1721
|
+
locusUrl: 'breakout-url',
|
|
1722
|
+
locusInfo: {
|
|
1723
|
+
info: {globalMeetingId: 'gmid-other'},
|
|
1724
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
1725
|
+
},
|
|
1726
|
+
sendCallAnalyzerMetrics: sinon.stub(),
|
|
1727
|
+
},
|
|
1728
|
+
};
|
|
1729
|
+
|
|
1730
|
+
webex.meetings.meetingCollection.getAll = sinon
|
|
1731
|
+
.stub()
|
|
1732
|
+
.returns(meetingCollectionMeetings);
|
|
1733
|
+
webex.meetings.request.getActiveMeetings = sinon.stub().resolves({
|
|
1734
|
+
loci: [{url: 'main-url', info: {globalMeetingId: 'gmid-123'}}],
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
await webex.meetings.syncMeetings();
|
|
1738
|
+
|
|
1739
|
+
assert.calledOnce(destroySpy);
|
|
1740
|
+
assert.calledWith(destroySpy, meetingCollectionMeetings.breakoutMeeting);
|
|
1741
|
+
});
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
describe('skipHashTreeSync parameter', () => {
|
|
1745
|
+
it('should skip syncAllHashTreeDatasets when skipHashTreeSync is true', async () => {
|
|
1746
|
+
const mockLocusInfo = {
|
|
1747
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1750
|
+
webex.meetings.request.getActiveMeetings = sinon.stub().resolves({loci: []});
|
|
1751
|
+
webex.meetings.meetingCollection.getAll = sinon.stub().returns({
|
|
1752
|
+
meeting1: {locusInfo: mockLocusInfo},
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false, skipHashTreeSync: true});
|
|
1756
|
+
|
|
1757
|
+
assert.calledOnce(webex.meetings.request.getActiveMeetings);
|
|
1758
|
+
assert.notCalled(mockLocusInfo.syncAllHashTreeDatasets);
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
it('should call syncAllHashTreeDatasets when skipHashTreeSync is false (default)', async () => {
|
|
1762
|
+
const mockLocusInfo = {
|
|
1763
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
1764
|
+
};
|
|
1765
|
+
|
|
1766
|
+
webex.meetings.request.getActiveMeetings = sinon.stub().resolves({loci: []});
|
|
1767
|
+
webex.meetings.meetingCollection.getAll = sinon.stub().returns({
|
|
1768
|
+
meeting1: {locusInfo: mockLocusInfo},
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false, skipHashTreeSync: false});
|
|
1772
|
+
|
|
1773
|
+
assert.calledOnce(webex.meetings.request.getActiveMeetings);
|
|
1774
|
+
assert.calledOnce(mockLocusInfo.syncAllHashTreeDatasets);
|
|
1775
|
+
});
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1778
|
+
describe('syncAllHashTreeDatasets in syncMeetings', () => {
|
|
1779
|
+
it('should call syncAllHashTreeDatasets for multiple meetings, skipping those without locusInfo', async () => {
|
|
1780
|
+
const mockLocusInfo1 = {
|
|
1781
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
1782
|
+
};
|
|
1783
|
+
const mockLocusInfo2 = {
|
|
1784
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
1785
|
+
};
|
|
1786
|
+
|
|
1787
|
+
webex.meetings.request.getActiveMeetings = sinon.stub().resolves({loci: []});
|
|
1788
|
+
webex.meetings.meetingCollection.getAll = sinon.stub().returns({
|
|
1789
|
+
meeting1: {locusInfo: mockLocusInfo1},
|
|
1790
|
+
meeting2: {locusInfo: undefined},
|
|
1791
|
+
meeting3: {locusInfo: mockLocusInfo2},
|
|
1792
|
+
meeting4: {},
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false});
|
|
1796
|
+
|
|
1797
|
+
assert.calledOnce(mockLocusInfo1.syncAllHashTreeDatasets);
|
|
1798
|
+
assert.calledOnce(mockLocusInfo2.syncAllHashTreeDatasets);
|
|
1799
|
+
});
|
|
1800
|
+
|
|
1801
|
+
it('should not call syncAllHashTreeDatasets when getActiveMeetings throws an error', async () => {
|
|
1802
|
+
const mockLocusInfo = {
|
|
1803
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
1804
|
+
};
|
|
1805
|
+
|
|
1806
|
+
webex.meetings.request.getActiveMeetings = sinon.stub().rejects(new Error('network error'));
|
|
1807
|
+
webex.meetings.meetingCollection.getAll = sinon.stub().returns({
|
|
1808
|
+
meeting1: {locusInfo: mockLocusInfo},
|
|
1809
|
+
});
|
|
1810
|
+
|
|
1811
|
+
try {
|
|
1812
|
+
await webex.meetings.syncMeetings();
|
|
1813
|
+
assert.fail('should have thrown');
|
|
1814
|
+
} catch (err) {
|
|
1815
|
+
assert.equal(err.message, 'network error');
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
assert.notCalled(mockLocusInfo.syncAllHashTreeDatasets);
|
|
1819
|
+
});
|
|
1820
|
+
});
|
|
1555
1821
|
});
|
|
1556
1822
|
describe('#fetchStaticMeetingLink', () => {
|
|
1557
1823
|
const conversationUrl = 'conv.fakeconversationurl.com';
|
|
@@ -2015,7 +2281,7 @@ describe('plugin-meetings', () => {
|
|
|
2015
2281
|
},
|
|
2016
2282
|
},
|
|
2017
2283
|
hashTreeMessage: undefined,
|
|
2018
|
-
});
|
|
2284
|
+
}, sinon.match.func);
|
|
2019
2285
|
});
|
|
2020
2286
|
it('should setup the meeting from a hash tree event', async () => {
|
|
2021
2287
|
const selfData = {};
|
|
@@ -2049,7 +2315,7 @@ describe('plugin-meetings', () => {
|
|
|
2049
2315
|
info: infoData,
|
|
2050
2316
|
},
|
|
2051
2317
|
hashTreeMessage,
|
|
2052
|
-
});
|
|
2318
|
+
}, sinon.match.func);
|
|
2053
2319
|
});
|
|
2054
2320
|
|
|
2055
2321
|
it('should ignore hash tree event when created locus has INACTIVE fullState', async () => {
|
|
@@ -2129,7 +2395,7 @@ describe('plugin-meetings', () => {
|
|
|
2129
2395
|
},
|
|
2130
2396
|
},
|
|
2131
2397
|
hashTreeMessage: undefined,
|
|
2132
|
-
});
|
|
2398
|
+
}, sinon.match.func);
|
|
2133
2399
|
});
|
|
2134
2400
|
|
|
2135
2401
|
it('sends client event correctly on finally', async () => {
|
|
@@ -2205,7 +2471,7 @@ describe('plugin-meetings', () => {
|
|
|
2205
2471
|
},
|
|
2206
2472
|
},
|
|
2207
2473
|
hashTreeMessage: undefined,
|
|
2208
|
-
});
|
|
2474
|
+
}, sinon.match.func);
|
|
2209
2475
|
});
|
|
2210
2476
|
|
|
2211
2477
|
const generateFakeLocusData = (isUnifiedSpaceMeeting) => ({
|
|
@@ -2833,6 +3099,39 @@ describe('plugin-meetings', () => {
|
|
|
2833
3099
|
checkCreateMeetingWithNoMeetingInfo(true, true);
|
|
2834
3100
|
});
|
|
2835
3101
|
|
|
3102
|
+
it('does not emit meeting:added when meeting is destroyed due to missing meeting info', async () => {
|
|
3103
|
+
// Make destroy actually remove the meeting from the collection
|
|
3104
|
+
// so that getMeetingByType returns null in the finally block
|
|
3105
|
+
webex.meetings.destroy = sinon.stub().callsFake((meeting) => {
|
|
3106
|
+
webex.meetings.meetingCollection.delete(meeting.id);
|
|
3107
|
+
});
|
|
3108
|
+
|
|
3109
|
+
try {
|
|
3110
|
+
await webex.meetings.createMeeting(
|
|
3111
|
+
'test destination',
|
|
3112
|
+
'test type',
|
|
3113
|
+
undefined,
|
|
3114
|
+
undefined,
|
|
3115
|
+
undefined,
|
|
3116
|
+
true
|
|
3117
|
+
);
|
|
3118
|
+
assert.fail('should have thrown NoMeetingInfoError');
|
|
3119
|
+
} catch (err) {
|
|
3120
|
+
assert.instanceOf(err, NoMeetingInfoError);
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
assert.calledOnce(webex.meetings.destroy);
|
|
3124
|
+
|
|
3125
|
+
// meeting:added should NOT have been triggered since the meeting was destroyed
|
|
3126
|
+
assert.neverCalledWith(
|
|
3127
|
+
TriggerProxy.trigger,
|
|
3128
|
+
sinon.match.any,
|
|
3129
|
+
sinon.match({function: 'createMeeting'}),
|
|
3130
|
+
'meeting:added',
|
|
3131
|
+
sinon.match.any
|
|
3132
|
+
);
|
|
3133
|
+
});
|
|
3134
|
+
|
|
2836
3135
|
it('creates the meeting avoiding meeting info fetch by passing type as DESTINATION_TYPE.ONE_ON_ONE_CALL', async () => {
|
|
2837
3136
|
const meeting = await webex.meetings.createMeeting(
|
|
2838
3137
|
'test destination',
|
|
@@ -2847,6 +3146,30 @@ describe('plugin-meetings', () => {
|
|
|
2847
3146
|
|
|
2848
3147
|
assert.notCalled(webex.meetings.meetingInfo.fetchMeetingInfo);
|
|
2849
3148
|
});
|
|
3149
|
+
|
|
3150
|
+
[
|
|
3151
|
+
{fullStateType: 'CALL'},
|
|
3152
|
+
{fullStateType: 'SIP_BRIDGE'},
|
|
3153
|
+
{fullStateType: 'SPACE_SHARE'},
|
|
3154
|
+
].forEach(({fullStateType}) => {
|
|
3155
|
+
it(`skips meeting info fetch when LOCUS_ID destination is a 1:1 call (fullState.type ${fullStateType})`, async () => {
|
|
3156
|
+
const locusDestination = {
|
|
3157
|
+
fullState: {type: fullStateType},
|
|
3158
|
+
};
|
|
3159
|
+
|
|
3160
|
+
const meeting = await webex.meetings.createMeeting(
|
|
3161
|
+
locusDestination,
|
|
3162
|
+
DESTINATION_TYPE.LOCUS_ID
|
|
3163
|
+
);
|
|
3164
|
+
|
|
3165
|
+
assert.instanceOf(
|
|
3166
|
+
meeting,
|
|
3167
|
+
Meeting,
|
|
3168
|
+
'createMeeting should eventually resolve to a Meeting Object'
|
|
3169
|
+
);
|
|
3170
|
+
assert.notCalled(webex.meetings.meetingInfo.fetchMeetingInfo);
|
|
3171
|
+
});
|
|
3172
|
+
});
|
|
2850
3173
|
});
|
|
2851
3174
|
|
|
2852
3175
|
describe('rejected MeetingInfo.#fetchMeetingInfo - does not log for known Error types', () => {
|
|
@@ -3426,6 +3749,21 @@ describe('plugin-meetings', () => {
|
|
|
3426
3749
|
'Meetings:index#isNeedHandleMainLocus --> self device left&moved in main locus with self joined status, not need to handle'
|
|
3427
3750
|
);
|
|
3428
3751
|
});
|
|
3752
|
+
|
|
3753
|
+
it('check breakout ended with self removed, return false', () => {
|
|
3754
|
+
webex.meetings.meetingCollection.getActiveBreakoutLocus = sinon.stub().returns(null);
|
|
3755
|
+
newLocus.self.state = 'LEFT';
|
|
3756
|
+
newLocus.self.reason = 'OTHER';
|
|
3757
|
+
newLocus.self.removed = true;
|
|
3758
|
+
newLocus.fullState = {state: 'INACTIVE', endMeetingReason: 'BREAKOUT_ENDED'};
|
|
3759
|
+
LoggerProxy.logger.log = sinon.stub();
|
|
3760
|
+
const result = webex.meetings.isNeedHandleMainLocus(meeting, newLocus);
|
|
3761
|
+
assert.equal(result, false);
|
|
3762
|
+
assert.calledWith(
|
|
3763
|
+
LoggerProxy.logger.log,
|
|
3764
|
+
'Meetings:index#isNeedHandleMainLocus --> self moved main locus with self removed status or with device resource moved, not need to handle'
|
|
3765
|
+
);
|
|
3766
|
+
});
|
|
3429
3767
|
});
|
|
3430
3768
|
|
|
3431
3769
|
describe('#isNeedHandleLocusDTO', () => {
|
|
@@ -3486,6 +3824,18 @@ describe('plugin-meetings', () => {
|
|
|
3486
3824
|
const result = webex.meetings.isNeedHandleLocusDTO(meeting, newLocus);
|
|
3487
3825
|
assert.equal(result, false);
|
|
3488
3826
|
});
|
|
3827
|
+
it('breakout session with breakout ended, return false', () => {
|
|
3828
|
+
newLocus.controls.breakout = {
|
|
3829
|
+
sessionType: 'BREAKOUT',
|
|
3830
|
+
};
|
|
3831
|
+
newLocus.self.state = 'LEFT';
|
|
3832
|
+
newLocus.self.reason = 'OTHER';
|
|
3833
|
+
newLocus.self.devices = [];
|
|
3834
|
+
newLocus.fullState = {state: 'INACTIVE', endMeetingReason: 'BREAKOUT_ENDED'};
|
|
3835
|
+
LoggerProxy.logger.log = sinon.stub();
|
|
3836
|
+
const result = webex.meetings.isNeedHandleLocusDTO(meeting, newLocus);
|
|
3837
|
+
assert.equal(result, false);
|
|
3838
|
+
});
|
|
3489
3839
|
it('moved to lobby, return true', () => {
|
|
3490
3840
|
newLocus.controls.breakout = {
|
|
3491
3841
|
sessionType: 'MAIN',
|