@webex/plugin-meetings 3.11.0-next.4 → 3.11.0-next.41

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 (146) hide show
  1. package/dist/aiEnableRequest/index.js +184 -0
  2. package/dist/aiEnableRequest/index.js.map +1 -0
  3. package/dist/aiEnableRequest/utils.js +36 -0
  4. package/dist/aiEnableRequest/utils.js.map +1 -0
  5. package/dist/annotation/index.js +3 -3
  6. package/dist/annotation/index.js.map +1 -1
  7. package/dist/breakouts/breakout.js +1 -1
  8. package/dist/breakouts/index.js +1 -1
  9. package/dist/config.js +5 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.js +26 -6
  12. package/dist/constants.js.map +1 -1
  13. package/dist/hashTree/constants.js +3 -1
  14. package/dist/hashTree/constants.js.map +1 -1
  15. package/dist/hashTree/hashTree.js +18 -0
  16. package/dist/hashTree/hashTree.js.map +1 -1
  17. package/dist/hashTree/hashTreeParser.js +709 -380
  18. package/dist/hashTree/hashTreeParser.js.map +1 -1
  19. package/dist/hashTree/types.js +4 -2
  20. package/dist/hashTree/types.js.map +1 -1
  21. package/dist/hashTree/utils.js +10 -0
  22. package/dist/hashTree/utils.js.map +1 -1
  23. package/dist/index.js +11 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/interceptors/constant.js +12 -0
  26. package/dist/interceptors/constant.js.map +1 -0
  27. package/dist/interceptors/dataChannelAuthToken.js +233 -0
  28. package/dist/interceptors/dataChannelAuthToken.js.map +1 -0
  29. package/dist/interceptors/index.js +7 -0
  30. package/dist/interceptors/index.js.map +1 -1
  31. package/dist/interpretation/index.js +2 -2
  32. package/dist/interpretation/index.js.map +1 -1
  33. package/dist/interpretation/siLanguage.js +1 -1
  34. package/dist/locus-info/controlsUtils.js +5 -3
  35. package/dist/locus-info/controlsUtils.js.map +1 -1
  36. package/dist/locus-info/index.js +125 -68
  37. package/dist/locus-info/index.js.map +1 -1
  38. package/dist/locus-info/selfUtils.js +1 -0
  39. package/dist/locus-info/selfUtils.js.map +1 -1
  40. package/dist/locus-info/types.js.map +1 -1
  41. package/dist/media/MediaConnectionAwaiter.js +57 -1
  42. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  43. package/dist/media/properties.js +4 -2
  44. package/dist/media/properties.js.map +1 -1
  45. package/dist/meeting/in-meeting-actions.js +7 -1
  46. package/dist/meeting/in-meeting-actions.js.map +1 -1
  47. package/dist/meeting/index.js +209 -90
  48. package/dist/meeting/index.js.map +1 -1
  49. package/dist/meeting/request.js +50 -0
  50. package/dist/meeting/request.js.map +1 -1
  51. package/dist/meeting/request.type.js.map +1 -1
  52. package/dist/meeting/util.js +128 -2
  53. package/dist/meeting/util.js.map +1 -1
  54. package/dist/meetings/index.js +78 -36
  55. package/dist/meetings/index.js.map +1 -1
  56. package/dist/member/index.js +10 -0
  57. package/dist/member/index.js.map +1 -1
  58. package/dist/member/util.js +10 -0
  59. package/dist/member/util.js.map +1 -1
  60. package/dist/metrics/constants.js +2 -1
  61. package/dist/metrics/constants.js.map +1 -1
  62. package/dist/multistream/mediaRequestManager.js +1 -1
  63. package/dist/multistream/mediaRequestManager.js.map +1 -1
  64. package/dist/multistream/remoteMediaManager.js +11 -0
  65. package/dist/multistream/remoteMediaManager.js.map +1 -1
  66. package/dist/reactions/reactions.type.js.map +1 -1
  67. package/dist/types/aiEnableRequest/index.d.ts +5 -0
  68. package/dist/types/aiEnableRequest/utils.d.ts +2 -0
  69. package/dist/types/config.d.ts +3 -0
  70. package/dist/types/constants.d.ts +21 -1
  71. package/dist/types/hashTree/constants.d.ts +1 -0
  72. package/dist/types/hashTree/hashTree.d.ts +7 -0
  73. package/dist/types/hashTree/hashTreeParser.d.ts +99 -14
  74. package/dist/types/hashTree/types.d.ts +3 -0
  75. package/dist/types/hashTree/utils.d.ts +6 -0
  76. package/dist/types/index.d.ts +1 -0
  77. package/dist/types/interceptors/constant.d.ts +5 -0
  78. package/dist/types/interceptors/dataChannelAuthToken.d.ts +35 -0
  79. package/dist/types/interceptors/index.d.ts +2 -1
  80. package/dist/types/locus-info/index.d.ts +9 -2
  81. package/dist/types/locus-info/types.d.ts +1 -0
  82. package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
  83. package/dist/types/media/properties.d.ts +2 -1
  84. package/dist/types/meeting/in-meeting-actions.d.ts +6 -0
  85. package/dist/types/meeting/index.d.ts +24 -2
  86. package/dist/types/meeting/request.d.ts +16 -1
  87. package/dist/types/meeting/request.type.d.ts +5 -0
  88. package/dist/types/meeting/util.d.ts +31 -0
  89. package/dist/types/meetings/index.d.ts +4 -2
  90. package/dist/types/member/index.d.ts +1 -0
  91. package/dist/types/member/util.d.ts +5 -0
  92. package/dist/types/metrics/constants.d.ts +1 -0
  93. package/dist/types/reactions/reactions.type.d.ts +1 -0
  94. package/dist/webinar/index.js +1 -1
  95. package/package.json +22 -22
  96. package/src/aiEnableRequest/README.md +84 -0
  97. package/src/aiEnableRequest/index.ts +170 -0
  98. package/src/aiEnableRequest/utils.ts +25 -0
  99. package/src/annotation/index.ts +7 -4
  100. package/src/config.ts +3 -0
  101. package/src/constants.ts +26 -1
  102. package/src/hashTree/constants.ts +1 -0
  103. package/src/hashTree/hashTree.ts +17 -0
  104. package/src/hashTree/hashTreeParser.ts +627 -249
  105. package/src/hashTree/types.ts +4 -0
  106. package/src/hashTree/utils.ts +9 -0
  107. package/src/index.ts +8 -1
  108. package/src/interceptors/constant.ts +6 -0
  109. package/src/interceptors/dataChannelAuthToken.ts +142 -0
  110. package/src/interceptors/index.ts +2 -1
  111. package/src/interpretation/index.ts +2 -2
  112. package/src/locus-info/controlsUtils.ts +11 -0
  113. package/src/locus-info/index.ts +146 -58
  114. package/src/locus-info/selfUtils.ts +1 -0
  115. package/src/locus-info/types.ts +1 -0
  116. package/src/media/MediaConnectionAwaiter.ts +41 -1
  117. package/src/media/properties.ts +3 -1
  118. package/src/meeting/in-meeting-actions.ts +12 -0
  119. package/src/meeting/index.ts +127 -17
  120. package/src/meeting/request.ts +42 -0
  121. package/src/meeting/request.type.ts +6 -0
  122. package/src/meeting/util.ts +156 -1
  123. package/src/meetings/index.ts +94 -9
  124. package/src/member/index.ts +10 -0
  125. package/src/member/util.ts +12 -0
  126. package/src/metrics/constants.ts +1 -0
  127. package/src/multistream/mediaRequestManager.ts +1 -1
  128. package/src/multistream/remoteMediaManager.ts +13 -0
  129. package/src/reactions/reactions.type.ts +1 -0
  130. package/test/unit/spec/aiEnableRequest/index.ts +981 -0
  131. package/test/unit/spec/aiEnableRequest/utils.ts +130 -0
  132. package/test/unit/spec/hashTree/hashTree.ts +66 -0
  133. package/test/unit/spec/hashTree/hashTreeParser.ts +1869 -189
  134. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +141 -0
  135. package/test/unit/spec/locus-info/controlsUtils.js +29 -0
  136. package/test/unit/spec/locus-info/index.js +201 -45
  137. package/test/unit/spec/media/MediaConnectionAwaiter.ts +41 -1
  138. package/test/unit/spec/media/properties.ts +12 -3
  139. package/test/unit/spec/meeting/in-meeting-actions.ts +8 -2
  140. package/test/unit/spec/meeting/index.js +441 -75
  141. package/test/unit/spec/meeting/request.js +64 -0
  142. package/test/unit/spec/meeting/utils.js +433 -22
  143. package/test/unit/spec/meetings/index.js +550 -10
  144. package/test/unit/spec/member/index.js +28 -4
  145. package/test/unit/spec/member/util.js +65 -27
  146. package/test/unit/spec/multistream/remoteMediaManager.ts +30 -0
@@ -0,0 +1,141 @@
1
+ import 'jsdom-global/register';
2
+ import {assert, expect} from '@webex/test-helper-chai';
3
+ import sinon from 'sinon';
4
+ import MockWebex from '@webex/test-helper-mock-webex';
5
+ import {WebexHttpError} from '@webex/webex-core';
6
+ import DataChannelAuthTokenInterceptor from '@webex/plugin-meetings/src/interceptors/dataChannelAuthToken';
7
+ import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
8
+ import {DATA_CHANNEL_AUTH_HEADER, MAX_RETRY} from '@webex/plugin-meetings/src/interceptors/constant';
9
+
10
+ describe('plugin-meetings', () => {
11
+ describe('Interceptors', () => {
12
+ describe('DataChannelAuthTokenInterceptor', () => {
13
+ let interceptor, webex, clock;
14
+
15
+ beforeEach(() => {
16
+ clock = sinon.useFakeTimers();
17
+
18
+ webex = new MockWebex({children: {}});
19
+ webex.request = sinon.stub().resolves({});
20
+
21
+ interceptor = Reflect.apply(DataChannelAuthTokenInterceptor.create, webex, []);
22
+
23
+ interceptor._refreshDataChannelToken = sinon.stub();
24
+ interceptor._isDataChannelTokenEnabled = sinon.stub().resolves(true);
25
+ });
26
+
27
+ afterEach(() => {
28
+ clock.restore();
29
+ });
30
+
31
+ const makeReason = (statusCode) =>
32
+ new WebexHttpError({
33
+ statusCode,
34
+ options: {headers: {}, uri: 'https://example.com'},
35
+ body: {},
36
+ });
37
+
38
+ describe('#onResponseError', () => {
39
+ it('rejects when no Data-Channel-Auth-Token header exists', async () => {
40
+ const options = {headers: {}};
41
+ const reason = makeReason(401);
42
+
43
+ await assert.isRejected(interceptor.onResponseError(options, reason), reason);
44
+ });
45
+
46
+ it('rejects when statusCode is not 401/403', async () => {
47
+ const options = {headers: {[DATA_CHANNEL_AUTH_HEADER]: 'abc'}};
48
+ const reason = makeReason(500);
49
+
50
+ await assert.isRejected(interceptor.onResponseError(options, reason), reason);
51
+ });
52
+
53
+ it('rejects when retry count exceeds MAX_RETRY', async () => {
54
+ const options = {headers: {[DATA_CHANNEL_AUTH_HEADER]: 'abc'}};
55
+ const reason = makeReason(401);
56
+
57
+ for (let i = 0; i < MAX_RETRY; i++) {
58
+ interceptor.onResponseError(options, reason).catch(() => {});
59
+ }
60
+
61
+ await assert.isRejected(interceptor.onResponseError(options, reason), reason);
62
+
63
+ sinon.assert.calledOnce(LoggerProxy.logger.error);
64
+ });
65
+
66
+ it('calls refreshTokenAndRetryWithDelay when eligible', async () => {
67
+ const options = {headers: {[DATA_CHANNEL_AUTH_HEADER]: 'abc'}};
68
+ const reason = makeReason(401);
69
+
70
+ interceptor._isDataChannelTokenEnabled.resolves(true);
71
+
72
+ const stub = sinon.stub(interceptor, 'refreshTokenAndRetryWithDelay').resolves('ok');
73
+
74
+ await interceptor.onResponseError(options, reason);
75
+
76
+ sinon.assert.calledOnceWithExactly(stub, options);
77
+ });
78
+
79
+ it('rejects when isDataChannelTokenEnabled is false', async () => {
80
+ const options = {headers: {[DATA_CHANNEL_AUTH_HEADER]: 'abc'}};
81
+ const reason = makeReason(401);
82
+
83
+ interceptor._isDataChannelTokenEnabled.resolves(false);
84
+
85
+ await assert.isRejected(interceptor.onResponseError(options, reason), reason);
86
+ });
87
+ });
88
+
89
+ describe('#refreshTokenAndRetryWithDelay', () => {
90
+ const options = {
91
+ headers: {[DATA_CHANNEL_AUTH_HEADER]: 'old-token'},
92
+ method: 'GET',
93
+ uri: 'https://example.com',
94
+ };
95
+
96
+ it('refreshes token and retries request successfully', async () => {
97
+ interceptor._refreshDataChannelToken.resolves('new-token');
98
+ webex.request.resolves('mock-response');
99
+
100
+ const promise = interceptor.refreshTokenAndRetryWithDelay(options);
101
+
102
+ clock.tick(2000);
103
+
104
+ const result = await promise;
105
+
106
+ expect(interceptor._refreshDataChannelToken.calledOnce).to.be.true;
107
+ expect(options.headers[DATA_CHANNEL_AUTH_HEADER]).to.equal('new-token');
108
+ expect(webex.request.calledOnceWith(options)).to.be.true;
109
+ expect(result).to.equal('mock-response');
110
+ });
111
+
112
+ it('rejects when refreshDataChannelToken fails', async () => {
113
+ interceptor._refreshDataChannelToken.rejects(new Error('refresh failed'));
114
+
115
+ const promise = interceptor.refreshTokenAndRetryWithDelay(options);
116
+
117
+ clock.tick(2000);
118
+
119
+ await assert.isRejected(
120
+ promise,
121
+ /DataChannel token refresh failed: refresh failed/
122
+ );
123
+ });
124
+
125
+ it('rejects when retry request fails', async () => {
126
+ interceptor._refreshDataChannelToken.resolves('new-token');
127
+ webex.request.rejects(new Error('request failed'));
128
+
129
+ const promise = interceptor.refreshTokenAndRetryWithDelay(options);
130
+
131
+ clock.tick(2000);
132
+
133
+ await assert.isRejected(
134
+ promise,
135
+ /DataChannel token refresh failed: request failed/
136
+ );
137
+ });
138
+ });
139
+ });
140
+ });
141
+ });
@@ -489,6 +489,35 @@ describe('plugin-meetings', () => {
489
489
  assert.equal(updates.hasHesiodLLMIdChanged, true);
490
490
  });
491
491
 
492
+ describe('hasAiSummaryNotificationChanged', () => {
493
+ it('returns false when aiSummaryNotification has not changed', () => {
494
+ const previous = {transcribe: {aiSummaryNotification: false}};
495
+ const current = {transcribe: {aiSummaryNotification: false}};
496
+ const {updates} = ControlsUtils.getControls(previous, current);
497
+ assert.equal(updates.hasAiSummaryNotificationChanged, false);
498
+ });
499
+
500
+ it('returns true when aiSummaryNotification changes from false to true', () => {
501
+ const previous = {transcribe: {aiSummaryNotification: false}};
502
+ const current = {transcribe: {aiSummaryNotification: true}};
503
+ const {updates} = ControlsUtils.getControls(previous, current);
504
+ assert.equal(updates.hasAiSummaryNotificationChanged, true);
505
+ });
506
+
507
+ it('returns true when aiSummaryNotification changes from undefined to true', () => {
508
+ const previous = {transcribe: undefined};
509
+ const current = {transcribe: {aiSummaryNotification: true}};
510
+ const {updates} = ControlsUtils.getControls(previous, current);
511
+ assert.equal(updates.hasAiSummaryNotificationChanged, true);
512
+ });
513
+
514
+ it('parses aiSummaryNotification into the transcribe block', () => {
515
+ const controls = {transcribe: {transcribing: false, caption: false, aiSummaryNotification: true}};
516
+ const parsed = ControlsUtils.parse(controls);
517
+ assert.equal(parsed.transcribe.aiSummaryNotification, true);
518
+ });
519
+ });
520
+
492
521
  describe('videoEnabled', () => {
493
522
  const testVideoEnabled = (oldControls, newControls, updatedProperty) => {
494
523
  const result = ControlsUtils.getControls(oldControls, newControls);
@@ -29,7 +29,8 @@ import {
29
29
  } from '../../../../src/constants';
30
30
 
31
31
  import {self, selfWithInactivity} from './selfConstant';
32
- import { MEETING_REMOVED_REASON } from '@webex/plugin-meetings/src/constants';
32
+ import {MEETING_REMOVED_REASON} from '@webex/plugin-meetings/src/constants';
33
+ import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
33
34
 
34
35
  describe('plugin-meetings', () => {
35
36
  describe('LocusInfo index', () => {
@@ -106,7 +107,7 @@ describe('plugin-meetings', () => {
106
107
  const createHashTreeMessage = (visibleDataSets) => ({
107
108
  locusStateElements: [
108
109
  {
109
- htMeta: {elementId: {type: 'self'}},
110
+ htMeta: {elementId: {type: 'metadata'}},
110
111
  data: {visibleDataSets},
111
112
  },
112
113
  ],
@@ -136,9 +137,13 @@ describe('plugin-meetings', () => {
136
137
  HashTreeParserStub,
137
138
  sinon.match({
138
139
  initialLocus: {
139
- locus: {self: {visibleDataSets}},
140
+ locus: null,
140
141
  dataSets: [],
141
142
  },
143
+ metadata: {
144
+ htMeta: hashTreeMessage.locusStateElements[0].htMeta,
145
+ visibleDataSets,
146
+ },
142
147
  webexRequest: sinon.match.func,
143
148
  locusInfoUpdateCallback: sinon.match.func,
144
149
  debugId: sinon.match.string,
@@ -169,11 +174,16 @@ describe('plugin-meetings', () => {
169
174
  const visibleDataSets = ['dataset1', 'dataset2'];
170
175
  const locus = createLocusWithVisibleDataSets(visibleDataSets);
171
176
  const dataSets = [{name: 'dataset1', url: 'http://dataset-url.com'}];
177
+ const metadata = {
178
+ htMeta: {elementId: {type: 'metadata'}},
179
+ visibleDataSets,
180
+ };
172
181
 
173
182
  await locusInfo.initialSetup({
174
183
  trigger: 'join-response',
175
184
  locus,
176
185
  dataSets,
186
+ metadata,
177
187
  });
178
188
 
179
189
  assert.calledOnceWithExactly(
@@ -183,6 +193,7 @@ describe('plugin-meetings', () => {
183
193
  locus,
184
194
  dataSets,
185
195
  },
196
+ metadata,
186
197
  webexRequest: sinon.match.func,
187
198
  locusInfoUpdateCallback: sinon.match.func,
188
199
  debugId: sinon.match.string,
@@ -220,12 +231,13 @@ describe('plugin-meetings', () => {
220
231
  HashTreeParserStub,
221
232
  sinon.match({
222
233
  initialLocus: {
223
- locus: {self: {visibleDataSets}},
234
+ locus: null,
224
235
  dataSets: [],
225
236
  },
226
237
  webexRequest: sinon.match.func,
227
238
  locusInfoUpdateCallback: sinon.match.func,
228
239
  debugId: sinon.match.string,
240
+ metadata: null,
229
241
  })
230
242
  );
231
243
  assert.calledOnceWithExactly(mockHashTreeParser.initializeFromGetLociResponse, locus);
@@ -249,6 +261,30 @@ describe('plugin-meetings', () => {
249
261
  assert.isTrue(locusInfo.emitChange);
250
262
  });
251
263
 
264
+ it('throws if called with "locus-message" and Metadata object without visibleDataSets', async () => {
265
+ const hashTreeMessage = {
266
+ locusStateElements: [
267
+ {
268
+ htMeta: {elementId: {type: 'Metadata'}},
269
+ data: {},
270
+ },
271
+ ],
272
+ dataSets: [{name: 'dataset1', url: 'test-url'}],
273
+ };
274
+ try {
275
+ await locusInfo.initialSetup({
276
+ trigger: 'locus-message',
277
+ hashTreeMessage,
278
+ });
279
+ assert.fail('should have thrown an error');
280
+ } catch (error) {
281
+ assert.equal(
282
+ error.message,
283
+ 'Metadata object with visibleDataSets is missing in the message'
284
+ );
285
+ }
286
+ });
287
+
252
288
  describe('should setup correct locusInfoUpdateCallback when creating HashTreeParser', () => {
253
289
  const OBJECTS_UPDATED = HashTreeParserModule.LocusInfoUpdateType.OBJECTS_UPDATED;
254
290
  const MEETING_ENDED = HashTreeParserModule.LocusInfoUpdateType.MEETING_ENDED;
@@ -265,8 +301,8 @@ describe('plugin-meetings', () => {
265
301
  hashTreeMessage: {
266
302
  locusStateElements: [
267
303
  {
268
- htMeta: {elementId: {type: 'self'}},
269
- data: {visibleDataSets: ['dataset1']},
304
+ htMeta: {elementId: {type: 'Metadata'}},
305
+ data: {visibleDataSets: [{name: 'dataset1', url: 'test-url'}]},
270
306
  },
271
307
  ],
272
308
  dataSets: [{name: 'dataset1', url: 'test-url'}],
@@ -293,6 +329,16 @@ describe('plugin-meetings', () => {
293
329
  htMeta: {elementId: {type: 'mediashare', id: 'fake-ht-mediaShare-2', version: 1}},
294
330
  },
295
331
  ];
332
+ locusInfo.embeddedApps = [
333
+ {
334
+ id: 'fake-embedded-app-1',
335
+ htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-1', version: 1}},
336
+ },
337
+ {
338
+ id: 'fake-embedded-app-2',
339
+ htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-2', version: 1}},
340
+ },
341
+ ];
296
342
  locusInfo.meetings = {id: 'fake-meetings'};
297
343
  locusInfo.participants = [
298
344
  {id: 'fake-participant-1', name: 'Participant One'},
@@ -328,6 +374,16 @@ describe('plugin-meetings', () => {
328
374
  htMeta: {elementId: {type: 'mediashare', id: 'fake-ht-mediaShare-2', version: 1}},
329
375
  },
330
376
  ],
377
+ embeddedApps: [
378
+ {
379
+ id: 'fake-embedded-app-1',
380
+ htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-1', version: 1}},
381
+ },
382
+ {
383
+ id: 'fake-embedded-app-2',
384
+ htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-2', version: 1}},
385
+ },
386
+ ],
331
387
  meetings: {id: 'fake-meetings'},
332
388
  jsSdkMeta: {removedParticipantIds: []},
333
389
  participants: [], // empty means there were no participant updates
@@ -504,6 +560,7 @@ describe('plugin-meetings', () => {
504
560
  self: {id: 'fake-self'},
505
561
  links: {id: 'fake-links'},
506
562
  mediaShares: expectedLocusInfo.mediaShares,
563
+ embeddedApps: expectedLocusInfo.embeddedApps,
507
564
  // and now the new fields
508
565
  ...newLocus,
509
566
  htMeta: newLocusHtMeta,
@@ -536,6 +593,7 @@ describe('plugin-meetings', () => {
536
593
  self: 'new-self',
537
594
  participants: 'new-participants',
538
595
  mediaShares: 'new-mediaShares',
596
+ embeddedApps: 'new-embeddedApps',
539
597
  },
540
598
  },
541
599
  ],
@@ -551,6 +609,7 @@ describe('plugin-meetings', () => {
551
609
  self: {id: 'fake-self'},
552
610
  links: {id: 'fake-links'},
553
611
  mediaShares: expectedLocusInfo.mediaShares,
612
+ embeddedApps: expectedLocusInfo.embeddedApps,
554
613
  participants: [], // empty means there were no participant updates
555
614
  jsSdkMeta: {removedParticipantIds: []}, // no participants were removed
556
615
  ...newLocus,
@@ -586,6 +645,7 @@ describe('plugin-meetings', () => {
586
645
  self: {id: 'fake-self'},
587
646
  links: {id: 'fake-links'},
588
647
  mediaShares: expectedLocusInfo.mediaShares,
648
+ embeddedApps: expectedLocusInfo.embeddedApps,
589
649
  // and now the new fields
590
650
  ...newLocus,
591
651
  htMeta: newLocusHtMeta,
@@ -725,6 +785,39 @@ describe('plugin-meetings', () => {
725
785
  });
726
786
  });
727
787
 
788
+ it('should process locus update correctly when called with updated EMBEDDEDAPP objects', () => {
789
+ const newEmbeddedApp = {
790
+ id: 'new-embedded-app-3',
791
+ htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-3', version: 100}},
792
+ };
793
+ const updatedEmbeddedApp2 = {
794
+ id: 'fake-embedded-app-2',
795
+ someNewProp: 'newValue',
796
+ htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-2', version: 100}},
797
+ };
798
+ // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
799
+ // with 1 embedded app added, 1 updated, and 1 removed
800
+ locusInfoUpdateCallback(OBJECTS_UPDATED, {
801
+ updatedObjects: [
802
+ {htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-1'}}, data: null},
803
+ {
804
+ htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-2'}},
805
+ data: updatedEmbeddedApp2,
806
+ },
807
+ {
808
+ htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-3'}},
809
+ data: newEmbeddedApp,
810
+ },
811
+ ],
812
+ });
813
+
814
+ // check onDeltaLocus() was called with correctly updated locus info
815
+ assert.calledOnceWithExactly(onDeltaLocusStub, {
816
+ ...expectedLocusInfo,
817
+ embeddedApps: [updatedEmbeddedApp2, newEmbeddedApp],
818
+ });
819
+ });
820
+
728
821
  it('should process locus update correctly when called with a combination of various updated objects', () => {
729
822
  const newSelf = {
730
823
  id: 'new-self',
@@ -1076,7 +1169,7 @@ describe('plugin-meetings', () => {
1076
1169
  it('should trigger the CONTROLS_POLLING_QA_CHANGED event when necessary', () => {
1077
1170
  locusInfo.controls = {};
1078
1171
  locusInfo.emitScoped = sinon.stub();
1079
- newControls.pollingQAControl = { enabled: true };
1172
+ newControls.pollingQAControl = {enabled: true};
1080
1173
  locusInfo.updateControls(newControls);
1081
1174
 
1082
1175
  assert.calledWith(
@@ -1366,6 +1459,34 @@ describe('plugin-meetings', () => {
1366
1459
  );
1367
1460
  });
1368
1461
 
1462
+ it('should emit CONTROLS_AI_SUMMARY_NOTIFICATION_UPDATED when aiSummaryNotification changes', () => {
1463
+ locusInfo.emitScoped = sinon.stub();
1464
+ locusInfo.controls = {
1465
+ transcribe: {
1466
+ transcribing: false,
1467
+ caption: false,
1468
+ aiSummaryNotification: false,
1469
+ },
1470
+ };
1471
+ newControls.transcribe.transcribing = false;
1472
+ newControls.transcribe.caption = false;
1473
+ newControls.transcribe.aiSummaryNotification = true;
1474
+
1475
+ locusInfo.updateControls(newControls);
1476
+
1477
+ assert.calledWith(
1478
+ locusInfo.emitScoped,
1479
+ {
1480
+ file: 'locus-info',
1481
+ function: 'updateControls',
1482
+ },
1483
+ LOCUSINFO.EVENTS.CONTROLS_AI_SUMMARY_NOTIFICATION_UPDATED,
1484
+ {
1485
+ aiSummaryNotification: true,
1486
+ }
1487
+ );
1488
+ });
1489
+
1369
1490
  it('should update the transcribe spoken language', () => {
1370
1491
  locusInfo.emitScoped = sinon.stub();
1371
1492
  locusInfo.controls = {
@@ -1631,7 +1752,6 @@ describe('plugin-meetings', () => {
1631
1752
  );
1632
1753
  });
1633
1754
 
1634
-
1635
1755
  it('should call with participant display name', () => {
1636
1756
  const failureParticipant = [
1637
1757
  {
@@ -1656,7 +1776,7 @@ describe('plugin-meetings', () => {
1656
1776
  displayName: 'Test User',
1657
1777
  }
1658
1778
  );
1659
- })
1779
+ });
1660
1780
  });
1661
1781
 
1662
1782
  describe('#updateSelf', () => {
@@ -2457,8 +2577,8 @@ describe('plugin-meetings', () => {
2457
2577
  {
2458
2578
  isInitializing: !self,
2459
2579
  }
2460
- );
2461
- });
2580
+ );
2581
+ });
2462
2582
 
2463
2583
  const checkMeetingInfoUpdatedCalled = (expected, payload) => {
2464
2584
  const expectedArgs = [
@@ -2923,28 +3043,28 @@ describe('plugin-meetings', () => {
2923
3043
  assert.isFunction(locusParser.onDeltaAction);
2924
3044
  });
2925
3045
 
2926
- it("#updateLocusInfo invokes updateLocusUrl before updateMeetingInfo", () => {
3046
+ it('#updateLocusInfo invokes updateLocusUrl before updateMeetingInfo', () => {
2927
3047
  const callOrder = [];
2928
- sinon.stub(locusInfo, "updateControls");
2929
- sinon.stub(locusInfo, "updateConversationUrl");
2930
- sinon.stub(locusInfo, "updateCreated");
2931
- sinon.stub(locusInfo, "updateFullState");
2932
- sinon.stub(locusInfo, "updateHostInfo");
2933
- sinon.stub(locusInfo, "updateMeetingInfo").callsFake(() => {
2934
- callOrder.push("updateMeetingInfo");
3048
+ sinon.stub(locusInfo, 'updateControls');
3049
+ sinon.stub(locusInfo, 'updateConversationUrl');
3050
+ sinon.stub(locusInfo, 'updateCreated');
3051
+ sinon.stub(locusInfo, 'updateFullState');
3052
+ sinon.stub(locusInfo, 'updateHostInfo');
3053
+ sinon.stub(locusInfo, 'updateMeetingInfo').callsFake(() => {
3054
+ callOrder.push('updateMeetingInfo');
2935
3055
  });
2936
- sinon.stub(locusInfo, "updateMediaShares");
2937
- sinon.stub(locusInfo, "updateReplaces");
2938
- sinon.stub(locusInfo, "updateSelf");
2939
- sinon.stub(locusInfo, "updateLocusUrl").callsFake(() => {
2940
- callOrder.push("updateLocusUrl");
3056
+ sinon.stub(locusInfo, 'updateMediaShares');
3057
+ sinon.stub(locusInfo, 'updateReplaces');
3058
+ sinon.stub(locusInfo, 'updateSelf');
3059
+ sinon.stub(locusInfo, 'updateLocusUrl').callsFake(() => {
3060
+ callOrder.push('updateLocusUrl');
2941
3061
  });
2942
- sinon.stub(locusInfo, "updateAclUrl");
2943
- sinon.stub(locusInfo, "updateBasequence");
2944
- sinon.stub(locusInfo, "updateSequence");
2945
- sinon.stub(locusInfo, "updateEmbeddedApps");
2946
- sinon.stub(locusInfo, "updateLinks");
2947
- sinon.stub(locusInfo, "compareAndUpdate");
3062
+ sinon.stub(locusInfo, 'updateAclUrl');
3063
+ sinon.stub(locusInfo, 'updateBasequence');
3064
+ sinon.stub(locusInfo, 'updateSequence');
3065
+ sinon.stub(locusInfo, 'updateEmbeddedApps');
3066
+ sinon.stub(locusInfo, 'updateLinks');
3067
+ sinon.stub(locusInfo, 'compareAndUpdate');
2948
3068
 
2949
3069
  locusInfo.updateLocusInfo(locus);
2950
3070
 
@@ -3000,7 +3120,7 @@ describe('plugin-meetings', () => {
3000
3120
  it('#updateLocusInfo puts the Locus DTO top level properties at the right place in LocusInfo class', () => {
3001
3121
  // this test verifies that the top-level properties of Locus DTO are copied
3002
3122
  // into LocusInfo class and set as top level properties too
3003
- // this is important, because the code handling Locus hass trees relies on it, see updateFromHashTree()
3123
+ // this is important, because the code handling Locus hash trees relies on it, see updateFromHashTree()
3004
3124
  const info = {id: 'info id'};
3005
3125
  const fullState = {id: 'fullState id'};
3006
3126
  const links = {services: {id: 'service links'}, resources: {id: 'resource links'}};
@@ -3039,7 +3159,7 @@ describe('plugin-meetings', () => {
3039
3159
  sandbox.stub(locusInfo, 'handleOneOnOneEvent');
3040
3160
  sandbox.stub(locusParser, 'isNewFullLocus').returns(true);
3041
3161
 
3042
- locusInfo.onFullLocus(fakeLocus, eventType);
3162
+ locusInfo.onFullLocus('test', fakeLocus, eventType);
3043
3163
 
3044
3164
  assert.equal(fakeLocus, locusParser.workingCopy);
3045
3165
  });
@@ -3060,7 +3180,7 @@ describe('plugin-meetings', () => {
3060
3180
 
3061
3181
  sandbox.stub(locusParser, 'isNewFullLocus').returns(false);
3062
3182
 
3063
- locusInfo.onFullLocus(fakeLocus, eventType);
3183
+ locusInfo.onFullLocus('test', fakeLocus, eventType);
3064
3184
 
3065
3185
  spies.forEach((spy) => {
3066
3186
  assert.notCalled(spy);
@@ -3210,7 +3330,11 @@ describe('plugin-meetings', () => {
3210
3330
  }).then(() => {
3211
3331
  assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'oldLocusUrl'});
3212
3332
 
3213
- assert.calledOnceWithExactly(meeting.locusInfo.onFullLocus, fakeFullLocusDto);
3333
+ assert.calledOnceWithExactly(
3334
+ meeting.locusInfo.onFullLocus,
3335
+ 'classic Locus sync',
3336
+ fakeFullLocusDto
3337
+ );
3214
3338
  assert.calledOnce(locusInfo.locusParser.resume);
3215
3339
  });
3216
3340
  });
@@ -3308,7 +3432,11 @@ describe('plugin-meetings', () => {
3308
3432
  });
3309
3433
 
3310
3434
  assert.notCalled(meeting.locusInfo.handleLocusDelta);
3311
- assert.calledOnceWithExactly(meeting.locusInfo.onFullLocus, fakeFullLocusDto);
3435
+ assert.calledOnceWithExactly(
3436
+ meeting.locusInfo.onFullLocus,
3437
+ 'classic Locus sync',
3438
+ fakeFullLocusDto
3439
+ );
3312
3440
  assert.calledOnce(locusInfo.locusParser.resume);
3313
3441
  });
3314
3442
  });
@@ -3484,7 +3612,11 @@ describe('plugin-meetings', () => {
3484
3612
  url: 'fake locus DELTA url',
3485
3613
  });
3486
3614
  assert.notCalled(meeting.locusInfo.handleLocusDelta);
3487
- assert.calledOnceWithExactly(meeting.locusInfo.onFullLocus, fakeFullLocusDto);
3615
+ assert.calledOnceWithExactly(
3616
+ meeting.locusInfo.onFullLocus,
3617
+ 'classic Locus sync',
3618
+ fakeFullLocusDto
3619
+ );
3488
3620
  assert.calledOnce(locusInfo.locusParser.resume);
3489
3621
  });
3490
3622
  });
@@ -3856,7 +3988,7 @@ describe('plugin-meetings', () => {
3856
3988
 
3857
3989
  describe('#updateLocusUrl', () => {
3858
3990
  it('trigger LOCUS_INFO_UPDATE_URL event with isMainLocus is true as default', () => {
3859
- const fakeUrl = "https://fake.com/locus";
3991
+ const fakeUrl = 'https://fake.com/locus';
3860
3992
  locusInfo.emitScoped = sinon.stub();
3861
3993
  locusInfo.updateLocusUrl(fakeUrl);
3862
3994
 
@@ -3869,12 +4001,12 @@ describe('plugin-meetings', () => {
3869
4001
  EVENTS.LOCUS_INFO_UPDATE_URL,
3870
4002
  {
3871
4003
  url: fakeUrl,
3872
- isMainLocus: true
3873
- },
4004
+ isMainLocus: true,
4005
+ }
3874
4006
  );
3875
4007
  });
3876
4008
  it('trigger LOCUS_INFO_UPDATE_URL event with isMainLocus is false', () => {
3877
- const fakeUrl = "https://fake.com/locus";
4009
+ const fakeUrl = 'https://fake.com/locus';
3878
4010
  locusInfo.emitScoped = sinon.stub();
3879
4011
  locusInfo.updateLocusUrl(fakeUrl, false);
3880
4012
 
@@ -3887,8 +4019,8 @@ describe('plugin-meetings', () => {
3887
4019
  EVENTS.LOCUS_INFO_UPDATE_URL,
3888
4020
  {
3889
4021
  url: fakeUrl,
3890
- isMainLocus: false
3891
- },
4022
+ isMainLocus: false,
4023
+ }
3892
4024
  );
3893
4025
  });
3894
4026
  });
@@ -3940,8 +4072,8 @@ describe('plugin-meetings', () => {
3940
4072
 
3941
4073
  sinon.stub(locusInfo, 'updateParticipants');
3942
4074
  sinon.stub(locusInfo, 'isMeetingActive');
3943
- sinon.stub(locusInfo, 'handleOneOnOneEvent');
3944
- (updateLocusInfoStub = sinon.stub(locusInfo, 'updateLocusInfo'));
4075
+ sinon.stub(locusInfo, 'handleOneOnOneEvent');
4076
+ updateLocusInfoStub = sinon.stub(locusInfo, 'updateLocusInfo');
3945
4077
  syncRequestStub = sinon.stub().resolves({body: {}});
3946
4078
 
3947
4079
  mockMeeting.locusInfo = locusInfo;
@@ -3950,7 +4082,7 @@ describe('plugin-meetings', () => {
3950
4082
  getLocusDTO: syncRequestStub,
3951
4083
  };
3952
4084
 
3953
- locusInfo.onFullLocus({
4085
+ locusInfo.onFullLocus('test', {
3954
4086
  sequence: {
3955
4087
  rangeStart: 0,
3956
4088
  rangeEnd: 0,
@@ -4213,6 +4345,30 @@ describe('plugin-meetings', () => {
4213
4345
 
4214
4346
  assert.calledOnceWithExactly(mockHashTreeParser.handleMessage, fakeHashTreeMessage);
4215
4347
  });
4348
+
4349
+ it('ignores hash tree event when hashTreeParser is not created yet', () => {
4350
+ const data = {
4351
+ eventType: LOCUSEVENT.HASH_TREE_DATA_UPDATED,
4352
+ stateElementsMessage: {
4353
+ locusStateElements: [],
4354
+ dataSets: [],
4355
+ },
4356
+ };
4357
+
4358
+ const loggerSpy = sinon.spy(LoggerProxy.logger, 'info');
4359
+ const getTheLocusToUpdateStub = sinon.stub(locusInfo, 'getTheLocusToUpdate');
4360
+
4361
+ // Ensure we're not using hash trees
4362
+ assert.isUndefined(locusInfo.hashTreeParser);
4363
+
4364
+ locusInfo.parse(mockMeeting, data);
4365
+
4366
+ assert.calledWith(
4367
+ loggerSpy,
4368
+ 'Locus-info:index#parse --> received locus hash tree event before hashTreeParser is created'
4369
+ );
4370
+ assert.notCalled(getTheLocusToUpdateStub);
4371
+ });
4216
4372
  });
4217
4373
  });
4218
4374
  });