@webex/plugin-meetings 3.12.0-next.10 → 3.12.0-next.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/aiEnableRequest/index.js +1 -1
  2. package/dist/breakouts/breakout.js +1 -1
  3. package/dist/breakouts/index.js +1 -1
  4. package/dist/controls-options-manager/constants.js +11 -1
  5. package/dist/controls-options-manager/constants.js.map +1 -1
  6. package/dist/controls-options-manager/index.js +23 -21
  7. package/dist/controls-options-manager/index.js.map +1 -1
  8. package/dist/controls-options-manager/util.js +91 -0
  9. package/dist/controls-options-manager/util.js.map +1 -1
  10. package/dist/hashTree/hashTreeParser.js +36 -20
  11. package/dist/hashTree/hashTreeParser.js.map +1 -1
  12. package/dist/interpretation/index.js +1 -1
  13. package/dist/interpretation/siLanguage.js +1 -1
  14. package/dist/locus-info/index.js +38 -14
  15. package/dist/locus-info/index.js.map +1 -1
  16. package/dist/meeting/util.js +1 -0
  17. package/dist/meeting/util.js.map +1 -1
  18. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  19. package/dist/types/hashTree/hashTreeParser.d.ts +12 -2
  20. package/dist/types/locus-info/index.d.ts +8 -3
  21. package/dist/webinar/index.js +1 -1
  22. package/package.json +13 -13
  23. package/src/controls-options-manager/constants.ts +14 -1
  24. package/src/controls-options-manager/index.ts +26 -19
  25. package/src/controls-options-manager/util.ts +81 -1
  26. package/src/hashTree/hashTreeParser.ts +39 -22
  27. package/src/locus-info/index.ts +48 -24
  28. package/src/meeting/util.ts +1 -0
  29. package/test/unit/spec/controls-options-manager/index.js +114 -6
  30. package/test/unit/spec/controls-options-manager/util.js +165 -0
  31. package/test/unit/spec/hashTree/hashTreeParser.ts +59 -32
  32. package/test/unit/spec/locus-info/index.js +47 -22
  33. package/test/unit/spec/meeting/utils.js +4 -0
@@ -1,5 +1,6 @@
1
1
  import ControlsOptionsManager from '@webex/plugin-meetings/src/controls-options-manager';
2
2
  import Util from '@webex/plugin-meetings/src/controls-options-manager/util';
3
+ import ParameterError from '@webex/plugin-meetings/src/common/errors/parameter';
3
4
  import sinon from 'sinon';
4
5
  import {assert} from '@webex/test-helper-chai';
5
6
  import { HTTP_VERBS } from '@webex/plugin-meetings/src/constants';
@@ -76,6 +77,19 @@ describe('plugin-meetings', () => {
76
77
 
77
78
  assert.deepEqual(result, request.request.firstCall.returnValue);
78
79
  });
80
+
81
+ it('should send setMuteOnEntry to locusUrl without authorizingLocusUrl when in breakout', () => {
82
+ manager.setDisplayHints(['ENABLE_MUTE_ON_ENTRY']);
83
+ manager.mainLocusUrl = 'test/main';
84
+
85
+ const result = manager.setMuteOnEntry(true);
86
+
87
+ assert.calledWith(request.request, { uri: 'test/id/controls',
88
+ body: { muteOnEntry: { enabled: true } },
89
+ method: HTTP_VERBS.PATCH});
90
+
91
+ assert.deepEqual(result, request.request.firstCall.returnValue);
92
+ });
79
93
  });
80
94
 
81
95
  describe('setDisallowUnmute', () => {
@@ -118,6 +132,19 @@ describe('plugin-meetings', () => {
118
132
 
119
133
  assert.deepEqual(result, request.request.firstCall.returnValue);
120
134
  });
135
+
136
+ it('should send setDisallowUnmute to locusUrl without authorizingLocusUrl when in breakout', () => {
137
+ manager.setDisplayHints(['ENABLE_HARD_MUTE']);
138
+ manager.mainLocusUrl = 'test/main';
139
+
140
+ const result = manager.setDisallowUnmute(true);
141
+
142
+ assert.calledWith(request.request, { uri: 'test/id/controls',
143
+ body: { disallowUnmute: { enabled: true } },
144
+ method: HTTP_VERBS.PATCH});
145
+
146
+ assert.deepEqual(result, request.request.firstCall.returnValue);
147
+ });
121
148
  });
122
149
  });
123
150
 
@@ -138,6 +165,18 @@ describe('plugin-meetings', () => {
138
165
  });
139
166
  });
140
167
 
168
+ it('should reject with ParameterError when locusUrl is not set', () => {
169
+ const noLocusManager = new ControlsOptionsManager(request);
170
+
171
+ const result = noLocusManager.update({scope: 'audio', properties: {muted: true}});
172
+
173
+ assert.notCalled(request.request);
174
+ return assert.isRejected(result).then((err) => {
175
+ assert.instanceOf(err, ParameterError);
176
+ assert.match(err.message, /locusUrl.*must be defined/);
177
+ });
178
+ });
179
+
141
180
  it('should throw an error if the scope is not supported', () => {
142
181
  const scope = 'invalid';
143
182
 
@@ -203,7 +242,7 @@ describe('plugin-meetings', () => {
203
242
  });
204
243
  });
205
244
 
206
- it('should call request with mainLocusUrl and locusUrl as authorizingLocusUrl if mainLocusUrl is exist and not same with locusUrl', () => {
245
+ it('should send audio controls to locusUrl without authorizingLocusUrl and non-audio to mainLocusUrl with authorizingLocusUrl when in breakout', () => {
207
246
  const restorable = Util.canUpdate;
208
247
  Util.canUpdate = sinon.stub().returns(true);
209
248
  manager.mainLocusUrl = 'test/main';
@@ -213,15 +252,16 @@ describe('plugin-meetings', () => {
213
252
 
214
253
  return manager.update(audio, reactions)
215
254
  .then(() => {
255
+ // Audio controls go directly to current locusUrl (no cross-locus authorization)
216
256
  assert.calledWith(request.request, {
217
- uri: 'test/main/controls',
257
+ uri: 'test/id/controls',
218
258
  body: {
219
259
  audio: audio.properties,
220
- authorizingLocusUrl: 'test/id'
221
260
  },
222
261
  method: HTTP_VERBS.PATCH,
223
262
  });
224
263
 
264
+ // Non-audio controls go to mainLocusUrl with authorizingLocusUrl
225
265
  assert.calledWith(request.request, {
226
266
  uri: 'test/main/controls',
227
267
  body: {
@@ -234,6 +274,49 @@ describe('plugin-meetings', () => {
234
274
  Util.canUpdate = restorable;
235
275
  });
236
276
  });
277
+
278
+ it('should send audio controls to locusUrl without authorizingLocusUrl when in breakout', () => {
279
+ const restorable = Util.canUpdate;
280
+ Util.canUpdate = sinon.stub().returns(true);
281
+ manager.mainLocusUrl = 'test/main';
282
+
283
+ const audio = {scope: 'audio', properties: {muted: true, disallowUnmute: false}};
284
+
285
+ return manager.update(audio)
286
+ .then(() => {
287
+ assert.calledWith(request.request, {
288
+ uri: 'test/id/controls',
289
+ body: {
290
+ audio: audio.properties,
291
+ },
292
+ method: HTTP_VERBS.PATCH,
293
+ });
294
+
295
+ Util.canUpdate = restorable;
296
+ });
297
+ });
298
+
299
+ it('should send non-audio controls to mainLocusUrl with authorizingLocusUrl when in breakout', () => {
300
+ const restorable = Util.canUpdate;
301
+ Util.canUpdate = sinon.stub().returns(true);
302
+ manager.mainLocusUrl = 'test/main';
303
+
304
+ const reactions = {scope: 'reactions', properties: {enabled: true}};
305
+
306
+ return manager.update(reactions)
307
+ .then(() => {
308
+ assert.calledWith(request.request, {
309
+ uri: 'test/main/controls',
310
+ body: {
311
+ reactions: reactions.properties,
312
+ authorizingLocusUrl: 'test/id',
313
+ },
314
+ method: HTTP_VERBS.PATCH,
315
+ });
316
+
317
+ Util.canUpdate = restorable;
318
+ });
319
+ });
237
320
  });
238
321
 
239
322
  describe('Mute/Unmute All', () => {
@@ -252,6 +335,18 @@ describe('plugin-meetings', () => {
252
335
  })
253
336
  });
254
337
 
338
+ it('should reject with ParameterError when locusUrl is not set', () => {
339
+ const noLocusManager = new ControlsOptionsManager(request);
340
+
341
+ const result = noLocusManager.setMuteAll(true, true, true);
342
+
343
+ assert.notCalled(request.request);
344
+ return assert.isRejected(result).then((err) => {
345
+ assert.instanceOf(err, ParameterError);
346
+ assert.match(err.message, /locusUrl.*must be defined/);
347
+ });
348
+ });
349
+
255
350
  it('rejects when correct display hint is not present mutedEnabled=false', () => {
256
351
  const result = manager.setMuteAll(false, false, false);
257
352
 
@@ -340,14 +435,27 @@ describe('plugin-meetings', () => {
340
435
  assert.deepEqual(result, request.request.firstCall.returnValue);
341
436
  });
342
437
 
343
- it('request with mainLocusUrl and make locusUrl as authorizingLocusUrl if mainLocusUrl is exist and not same with locusUrl', () => {
438
+ it('should send setMuteAll to locusUrl without authorizingLocusUrl when in breakout', () => {
344
439
  manager.setDisplayHints(['MUTE_ALL', 'DISABLE_HARD_MUTE', 'DISABLE_MUTE_ON_ENTRY']);
345
440
  manager.mainLocusUrl = `test/main`;
346
441
 
347
442
  const result = manager.setMuteAll(true, true, true, ['attendee']);
348
443
 
349
- assert.calledWith(request.request, { uri: 'test/main/controls',
350
- body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true, roles: ['attendee'] }, authorizingLocusUrl: 'test/id' },
444
+ assert.calledWith(request.request, { uri: 'test/id/controls',
445
+ body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true, roles: ['attendee'] } },
446
+ method: HTTP_VERBS.PATCH});
447
+
448
+ assert.deepEqual(result, request.request.firstCall.returnValue);
449
+ });
450
+
451
+ it('should send setMuteAll with PANELIST role to locusUrl without authorizingLocusUrl when in breakout', () => {
452
+ manager.setDisplayHints(['MUTE_ALL', 'ENABLE_HARD_MUTE', 'ENABLE_MUTE_ON_ENTRY']);
453
+ manager.mainLocusUrl = `test/main`;
454
+
455
+ const result = manager.setMuteAll(true, true, true, ['PANELIST']);
456
+
457
+ assert.calledWith(request.request, { uri: 'test/id/controls',
458
+ body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true, roles: ['PANELIST'] } },
351
459
  method: HTTP_VERBS.PATCH});
352
460
 
353
461
  assert.deepEqual(result, request.request.firstCall.returnValue);
@@ -799,6 +799,171 @@ describe('plugin-meetings', () => {
799
799
  );
800
800
  });
801
801
  });
802
+
803
+ describe('isAudioControl()', () => {
804
+ it('should return true when all body keys are audio control keys', () => {
805
+ assert.isTrue(ControlsOptionsUtil.isAudioControl({audio: {muted: true}}));
806
+ });
807
+
808
+ it('should return true when body has muteOnEntry key', () => {
809
+ assert.isTrue(ControlsOptionsUtil.isAudioControl({muteOnEntry: {enabled: true}}));
810
+ });
811
+
812
+ it('should return true when body has disallowUnmute key', () => {
813
+ assert.isTrue(ControlsOptionsUtil.isAudioControl({disallowUnmute: {enabled: true}}));
814
+ });
815
+
816
+ it('should return true when body has multiple audio control keys', () => {
817
+ assert.isTrue(ControlsOptionsUtil.isAudioControl({audio: {muted: true}, muteOnEntry: {enabled: true}, disallowUnmute: {enabled: true}}));
818
+ });
819
+
820
+ it('should return false when body has a non-audio control key', () => {
821
+ assert.isFalse(ControlsOptionsUtil.isAudioControl({raiseHand: {enabled: true}}));
822
+ });
823
+
824
+ it('should return false when body has a mix of audio and non-audio keys', () => {
825
+ assert.isFalse(ControlsOptionsUtil.isAudioControl({audio: {muted: true}, raiseHand: {enabled: true}}));
826
+ });
827
+
828
+ it('should return true for an empty body', () => {
829
+ assert.isTrue(ControlsOptionsUtil.isAudioControl({}));
830
+ });
831
+ });
832
+
833
+ describe('isBreakoutLocusUrl()', () => {
834
+ it('should return true when mainLocusUrl differs from locusUrl', () => {
835
+ assert.isTrue(ControlsOptionsUtil.isBreakoutLocusUrl('locus/breakout', 'locus/main'));
836
+ });
837
+
838
+ it('should return false when mainLocusUrl equals locusUrl', () => {
839
+ assert.isFalse(ControlsOptionsUtil.isBreakoutLocusUrl('locus/main', 'locus/main'));
840
+ });
841
+
842
+ it('should return false when mainLocusUrl is undefined', () => {
843
+ assert.isFalse(ControlsOptionsUtil.isBreakoutLocusUrl('locus/breakout', undefined));
844
+ });
845
+
846
+ it('should return false when mainLocusUrl is null', () => {
847
+ assert.isFalse(ControlsOptionsUtil.isBreakoutLocusUrl('locus/breakout', null));
848
+ });
849
+
850
+ it('should return false when mainLocusUrl is empty string', () => {
851
+ assert.isFalse(ControlsOptionsUtil.isBreakoutLocusUrl('locus/breakout', ''));
852
+ });
853
+ });
854
+
855
+ describe('getControlsRequestParams()', () => {
856
+ const locusUrl = 'locus/breakout';
857
+ const mainLocusUrl = 'locus/main';
858
+
859
+ it('should return full request params targeting locusUrl when not in a breakout', () => {
860
+ const result = ControlsOptionsUtil.getControlsRequestParams({
861
+ body: {raiseHand: {enabled: true}},
862
+ locusUrl: 'locus/main',
863
+ mainLocusUrl: 'locus/main',
864
+ });
865
+
866
+ assert.equal(result.uri, 'locus/main/controls');
867
+ assert.deepEqual(result.body, {raiseHand: {enabled: true}});
868
+ assert.equal(result.method, 'PATCH');
869
+ });
870
+
871
+ it('should return mainLocusUrl with authorizingLocusUrl in body for non-audio controls in a breakout', () => {
872
+ const result = ControlsOptionsUtil.getControlsRequestParams({
873
+ body: {raiseHand: {enabled: true}},
874
+ locusUrl,
875
+ mainLocusUrl,
876
+ });
877
+
878
+ assert.equal(result.uri, 'locus/main/controls');
879
+ assert.deepEqual(result.body, {raiseHand: {enabled: true}, authorizingLocusUrl: locusUrl});
880
+ assert.equal(result.method, 'PATCH');
881
+ });
882
+
883
+ it('should return locusUrl without authorizingLocusUrl for audio controls in a breakout', () => {
884
+ const result = ControlsOptionsUtil.getControlsRequestParams({
885
+ body: {audio: {muted: true}},
886
+ locusUrl,
887
+ mainLocusUrl,
888
+ });
889
+
890
+ assert.equal(result.uri, 'locus/breakout/controls');
891
+ assert.deepEqual(result.body, {audio: {muted: true}});
892
+ assert.equal(result.method, 'PATCH');
893
+ });
894
+
895
+ it('should return locusUrl without authorizingLocusUrl for muteOnEntry in a breakout', () => {
896
+ const result = ControlsOptionsUtil.getControlsRequestParams({
897
+ body: {muteOnEntry: {enabled: true}},
898
+ locusUrl,
899
+ mainLocusUrl,
900
+ });
901
+
902
+ assert.equal(result.uri, 'locus/breakout/controls');
903
+ assert.deepEqual(result.body, {muteOnEntry: {enabled: true}});
904
+ assert.equal(result.method, 'PATCH');
905
+ });
906
+
907
+ it('should return locusUrl without authorizingLocusUrl for disallowUnmute in a breakout', () => {
908
+ const result = ControlsOptionsUtil.getControlsRequestParams({
909
+ body: {disallowUnmute: {enabled: true}},
910
+ locusUrl,
911
+ mainLocusUrl,
912
+ });
913
+
914
+ assert.equal(result.uri, 'locus/breakout/controls');
915
+ assert.deepEqual(result.body, {disallowUnmute: {enabled: true}});
916
+ assert.equal(result.method, 'PATCH');
917
+ });
918
+
919
+ it('should return locusUrl when mainLocusUrl is undefined', () => {
920
+ const result = ControlsOptionsUtil.getControlsRequestParams({
921
+ body: {raiseHand: {enabled: true}},
922
+ locusUrl,
923
+ mainLocusUrl: undefined,
924
+ });
925
+
926
+ assert.equal(result.uri, 'locus/breakout/controls');
927
+ assert.deepEqual(result.body, {raiseHand: {enabled: true}});
928
+ assert.equal(result.method, 'PATCH');
929
+ });
930
+
931
+ it('should return locusUrl when mainLocusUrl is null', () => {
932
+ const result = ControlsOptionsUtil.getControlsRequestParams({
933
+ body: {raiseHand: {enabled: true}},
934
+ locusUrl,
935
+ mainLocusUrl: null,
936
+ });
937
+
938
+ assert.equal(result.uri, 'locus/breakout/controls');
939
+ assert.deepEqual(result.body, {raiseHand: {enabled: true}});
940
+ assert.equal(result.method, 'PATCH');
941
+ });
942
+
943
+ it('should return locusUrl when mainLocusUrl is empty string', () => {
944
+ const result = ControlsOptionsUtil.getControlsRequestParams({
945
+ body: {raiseHand: {enabled: true}},
946
+ locusUrl,
947
+ mainLocusUrl: '',
948
+ });
949
+
950
+ assert.equal(result.uri, 'locus/breakout/controls');
951
+ assert.deepEqual(result.body, {raiseHand: {enabled: true}});
952
+ assert.equal(result.method, 'PATCH');
953
+ });
954
+
955
+ it('should return locusUrl for audio controls when not in a breakout', () => {
956
+ const result = ControlsOptionsUtil.getControlsRequestParams({
957
+ body: {audio: {muted: true}},
958
+ locusUrl: 'locus/main',
959
+ mainLocusUrl: 'locus/main',
960
+ });
961
+
962
+ assert.equal(result.uri, 'locus/main/controls');
963
+ assert.deepEqual(result.body, {audio: {muted: true}});
964
+ assert.equal(result.method, 'PATCH');
965
+ });
966
+ });
802
967
  });
803
968
  });
804
969
  });
@@ -553,8 +553,7 @@ describe('HashTreeParser', () => {
553
553
  );
554
554
 
555
555
  // Verify callback was called with OBJECTS_UPDATED and correct updatedObjects list
556
- // Note: main is initialized before self due to sortByInitPriority
557
- assert.calledWith(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
556
+ assert.calledWith(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
558
557
  updatedObjects: [
559
558
  {
560
559
  htMeta: {
@@ -824,7 +823,7 @@ describe('HashTreeParser', () => {
824
823
  expect(parser.dataSets.self.version).to.equal(2100);
825
824
  expect(parser.dataSets['atd-unmuted'].version).to.equal(3100);
826
825
 
827
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
826
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
828
827
  updatedObjects: [
829
828
  {
830
829
  htMeta: {
@@ -955,7 +954,7 @@ describe('HashTreeParser', () => {
955
954
  {type: 'ControlEntry', id: 10101, version: 100}
956
955
  ]);
957
956
 
958
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
957
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
959
958
  updatedObjects: [
960
959
  {
961
960
  htMeta: {
@@ -1045,7 +1044,7 @@ describe('HashTreeParser', () => {
1045
1044
  assert.calledOnceWithExactly(mainPutItemsSpy, [{type: 'locus', id: 0, version: 201}]);
1046
1045
 
1047
1046
  // Verify callback was called only for known dataset
1048
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
1047
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
1049
1048
  updatedObjects: [
1050
1049
  {
1051
1050
  htMeta: {
@@ -1145,7 +1144,7 @@ describe('HashTreeParser', () => {
1145
1144
  assert.calledOnceWithExactly(selfPutItemSpy, {type: 'metadata', id: 5, version: 51});
1146
1145
 
1147
1146
  // Verify callback was called with metadata object and removed dataset objects
1148
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
1147
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
1149
1148
  updatedObjects: [
1150
1149
  // updated metadata object:
1151
1150
  {
@@ -1306,7 +1305,7 @@ describe('HashTreeParser', () => {
1306
1305
  assert.notCalled(atdUnmutedPutItemsSpy);
1307
1306
 
1308
1307
  // Verify callback was called with the updated object
1309
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
1308
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
1310
1309
  updatedObjects: [
1311
1310
  {
1312
1311
  htMeta: {
@@ -1534,7 +1533,7 @@ describe('HashTreeParser', () => {
1534
1533
  ]);
1535
1534
 
1536
1535
  // Verify callback was called with OBJECTS_UPDATED and all updated objects
1537
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
1536
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
1538
1537
  updatedObjects: [
1539
1538
  {
1540
1539
  htMeta: {
@@ -1599,9 +1598,7 @@ describe('HashTreeParser', () => {
1599
1598
  parser.handleMessage(sentinelMessage, 'sentinel message');
1600
1599
 
1601
1600
  // Verify callback was called with MEETING_ENDED
1602
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
1603
- updatedObjects: undefined,
1604
- });
1601
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
1605
1602
 
1606
1603
  // Verify that all timers were stopped
1607
1604
  Object.values(parser.dataSets).forEach((ds: any) => {
@@ -1623,9 +1620,7 @@ describe('HashTreeParser', () => {
1623
1620
  parser.handleMessage(sentinelMessage, 'sentinel message');
1624
1621
 
1625
1622
  // Verify callback was called with MEETING_ENDED
1626
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
1627
- updatedObjects: undefined,
1628
- });
1623
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
1629
1624
 
1630
1625
  // Verify that all timers were stopped
1631
1626
  Object.values(parser.dataSets).forEach((ds: any) => {
@@ -1721,7 +1716,7 @@ describe('HashTreeParser', () => {
1721
1716
  );
1722
1717
 
1723
1718
  // Verify that callback was called with synced objects
1724
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
1719
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
1725
1720
  updatedObjects: [
1726
1721
  {
1727
1722
  htMeta: {
@@ -1783,9 +1778,7 @@ describe('HashTreeParser', () => {
1783
1778
  await clock.tickAsync(1000);
1784
1779
 
1785
1780
  // Verify callback was called with MEETING_ENDED
1786
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
1787
- updatedObjects: undefined,
1788
- });
1781
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
1789
1782
 
1790
1783
  // Verify all timers are stopped
1791
1784
  Object.values(parser.dataSets).forEach((ds: any) => {
@@ -1848,9 +1841,7 @@ describe('HashTreeParser', () => {
1848
1841
  await clock.tickAsync(1000);
1849
1842
 
1850
1843
  // Verify callback was called with MEETING_ENDED
1851
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
1852
- updatedObjects: undefined,
1853
- });
1844
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
1854
1845
 
1855
1846
  // Verify all timers are stopped
1856
1847
  Object.values(parser.dataSets).forEach((ds: any) => {
@@ -2088,7 +2079,7 @@ describe('HashTreeParser', () => {
2088
2079
  assert.equal(parser.dataSets.attendees.hashTree.numLeaves, 8);
2089
2080
 
2090
2081
  // Verify callback was called with the metadata update (appears twice - processed once for visible dataset changes, once in main loop)
2091
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
2082
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
2092
2083
  updatedObjects: [
2093
2084
  {
2094
2085
  htMeta: {
@@ -2366,9 +2357,7 @@ describe('HashTreeParser', () => {
2366
2357
  await clock.tickAsync(0);
2367
2358
 
2368
2359
  // Verify callback was called with MEETING_ENDED
2369
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
2370
- updatedObjects: undefined,
2371
- });
2360
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
2372
2361
  });
2373
2362
 
2374
2363
  it('handles removal of visible data set', async () => {
@@ -2431,7 +2420,7 @@ describe('HashTreeParser', () => {
2431
2420
  assert.isUndefined(parser.dataSets['atd-unmuted'].timer);
2432
2421
 
2433
2422
  // Verify callback was called with the metadata update and the removed objects (metadata appears twice - processed once for dataset changes, once in main loop)
2434
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
2423
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
2435
2424
  updatedObjects: [
2436
2425
  {
2437
2426
  htMeta: {
@@ -3050,7 +3039,7 @@ describe('HashTreeParser', () => {
3050
3039
  parser.handleMessage(updateMessage, 'update with newer version');
3051
3040
 
3052
3041
  // Callback should be called with the update
3053
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
3042
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
3054
3043
  updatedObjects: [
3055
3044
  {
3056
3045
  htMeta: {
@@ -3121,7 +3110,7 @@ describe('HashTreeParser', () => {
3121
3110
  parser.handleMessage(removalMessage, 'removal of non-existent object');
3122
3111
 
3123
3112
  // Callback should be called with the removal
3124
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
3113
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
3125
3114
  updatedObjects: [
3126
3115
  {
3127
3116
  htMeta: {
@@ -3256,7 +3245,7 @@ describe('HashTreeParser', () => {
3256
3245
  parser.handleMessage(mixedMessage, 'mixed updates');
3257
3246
 
3258
3247
  // Callback should be called with only the valid updates (participant 1 v110 and participant 3 v10)
3259
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
3248
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
3260
3249
  updatedObjects: [
3261
3250
  {
3262
3251
  htMeta: {
@@ -3434,9 +3423,7 @@ describe('HashTreeParser', () => {
3434
3423
  parser.handleMessage(sentinelMessage as any, 'sentinel message');
3435
3424
 
3436
3425
  // Callback should be called with MEETING_ENDED
3437
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
3438
- updatedObjects: undefined,
3439
- });
3426
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
3440
3427
  });
3441
3428
  });
3442
3429
 
@@ -3795,4 +3782,44 @@ describe('HashTreeParser', () => {
3795
3782
  assert.notCalled(callback);
3796
3783
  });
3797
3784
  });
3785
+
3786
+ describe('#cleanUp', () => {
3787
+ it('should stop the parser, clear all timers and clear all dataSets', () => {
3788
+ const parser = createHashTreeParser();
3789
+
3790
+ // Send a message to set up sync timers via runSyncAlgorithm
3791
+ const message = {
3792
+ dataSets: [
3793
+ {
3794
+ ...createDataSet('main', 16, 1100),
3795
+ root: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1',
3796
+ },
3797
+ ],
3798
+ visibleDataSetsUrl,
3799
+ locusUrl,
3800
+ heartbeatIntervalMs: 5000,
3801
+ locusStateElements: [
3802
+ {
3803
+ htMeta: {
3804
+ elementId: {type: 'locus' as const, id: 0, version: 201},
3805
+ dataSetNames: ['main'],
3806
+ },
3807
+ data: {someData: 'value'},
3808
+ },
3809
+ ],
3810
+ };
3811
+
3812
+ parser.handleMessage(message, 'setup timers');
3813
+
3814
+ // Verify timers were set by handleMessage
3815
+ expect(parser.dataSets.main.timer).to.not.be.undefined;
3816
+ expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
3817
+
3818
+ parser.cleanUp();
3819
+
3820
+ expect(parser.state).to.equal('stopped');
3821
+ expect(parser.visibleDataSets).to.deep.equal([]);
3822
+ expect(parser.dataSets).to.deep.equal({});
3823
+ });
3824
+ });
3798
3825
  });