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