@webex/plugin-meetings 3.0.0-beta.39 → 3.0.0-beta.391

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 (393) hide show
  1. package/README.md +58 -8
  2. package/dist/annotation/annotation.types.js +7 -0
  3. package/dist/annotation/annotation.types.js.map +1 -0
  4. package/dist/annotation/constants.js +49 -0
  5. package/dist/annotation/constants.js.map +1 -0
  6. package/dist/annotation/index.js +342 -0
  7. package/dist/annotation/index.js.map +1 -0
  8. package/dist/breakouts/breakout.js +94 -15
  9. package/dist/breakouts/breakout.js.map +1 -1
  10. package/dist/breakouts/events.js +45 -0
  11. package/dist/breakouts/events.js.map +1 -0
  12. package/dist/breakouts/index.js +671 -81
  13. package/dist/breakouts/index.js.map +1 -1
  14. package/dist/breakouts/utils.js +45 -1
  15. package/dist/breakouts/utils.js.map +1 -1
  16. package/dist/common/errors/no-meeting-info.js +51 -0
  17. package/dist/common/errors/no-meeting-info.js.map +1 -0
  18. package/dist/common/errors/reclaim-host-role-errors.js +158 -0
  19. package/dist/common/errors/reclaim-host-role-errors.js.map +1 -0
  20. package/dist/common/errors/webex-errors.js +48 -7
  21. package/dist/common/errors/webex-errors.js.map +1 -1
  22. package/dist/common/logs/logger-proxy.js +1 -1
  23. package/dist/common/logs/logger-proxy.js.map +1 -1
  24. package/dist/common/logs/request.js +5 -1
  25. package/dist/common/logs/request.js.map +1 -1
  26. package/dist/common/queue.js +24 -9
  27. package/dist/common/queue.js.map +1 -1
  28. package/dist/config.js +5 -10
  29. package/dist/config.js.map +1 -1
  30. package/dist/constants.js +242 -33
  31. package/dist/constants.js.map +1 -1
  32. package/dist/controls-options-manager/enums.js +14 -2
  33. package/dist/controls-options-manager/enums.js.map +1 -1
  34. package/dist/controls-options-manager/index.js +109 -15
  35. package/dist/controls-options-manager/index.js.map +1 -1
  36. package/dist/controls-options-manager/types.js +7 -0
  37. package/dist/controls-options-manager/types.js.map +1 -0
  38. package/dist/controls-options-manager/util.js +309 -18
  39. package/dist/controls-options-manager/util.js.map +1 -1
  40. package/dist/index.js +110 -2
  41. package/dist/index.js.map +1 -1
  42. package/dist/interceptors/index.js +15 -0
  43. package/dist/interceptors/index.js.map +1 -0
  44. package/dist/interceptors/locusRetry.js +93 -0
  45. package/dist/interceptors/locusRetry.js.map +1 -0
  46. package/dist/interpretation/collection.js +23 -0
  47. package/dist/interpretation/collection.js.map +1 -0
  48. package/dist/interpretation/index.js +380 -0
  49. package/dist/interpretation/index.js.map +1 -0
  50. package/dist/interpretation/siLanguage.js +25 -0
  51. package/dist/interpretation/siLanguage.js.map +1 -0
  52. package/dist/locus-info/controlsUtils.js +91 -2
  53. package/dist/locus-info/controlsUtils.js.map +1 -1
  54. package/dist/locus-info/index.js +386 -62
  55. package/dist/locus-info/index.js.map +1 -1
  56. package/dist/locus-info/infoUtils.js +7 -1
  57. package/dist/locus-info/infoUtils.js.map +1 -1
  58. package/dist/locus-info/mediaSharesUtils.js +71 -1
  59. package/dist/locus-info/mediaSharesUtils.js.map +1 -1
  60. package/dist/locus-info/parser.js +249 -72
  61. package/dist/locus-info/parser.js.map +1 -1
  62. package/dist/locus-info/selfUtils.js +89 -14
  63. package/dist/locus-info/selfUtils.js.map +1 -1
  64. package/dist/media/index.js +65 -102
  65. package/dist/media/index.js.map +1 -1
  66. package/dist/media/properties.js +73 -124
  67. package/dist/media/properties.js.map +1 -1
  68. package/dist/mediaQualityMetrics/config.js +135 -330
  69. package/dist/mediaQualityMetrics/config.js.map +1 -1
  70. package/dist/meeting/in-meeting-actions.js +86 -2
  71. package/dist/meeting/in-meeting-actions.js.map +1 -1
  72. package/dist/meeting/index.js +4075 -2827
  73. package/dist/meeting/index.js.map +1 -1
  74. package/dist/meeting/locusMediaRequest.js +292 -0
  75. package/dist/meeting/locusMediaRequest.js.map +1 -0
  76. package/dist/meeting/muteState.js +224 -136
  77. package/dist/meeting/muteState.js.map +1 -1
  78. package/dist/meeting/request.js +177 -152
  79. package/dist/meeting/request.js.map +1 -1
  80. package/dist/meeting/util.js +672 -417
  81. package/dist/meeting/util.js.map +1 -1
  82. package/dist/meeting-info/index.js +73 -7
  83. package/dist/meeting-info/index.js.map +1 -1
  84. package/dist/meeting-info/meeting-info-v2.js +192 -51
  85. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  86. package/dist/meeting-info/util.js +1 -1
  87. package/dist/meeting-info/util.js.map +1 -1
  88. package/dist/meeting-info/utilv2.js +36 -36
  89. package/dist/meeting-info/utilv2.js.map +1 -1
  90. package/dist/meetings/collection.js +39 -0
  91. package/dist/meetings/collection.js.map +1 -1
  92. package/dist/meetings/index.js +484 -119
  93. package/dist/meetings/index.js.map +1 -1
  94. package/dist/meetings/meetings.types.js +7 -0
  95. package/dist/meetings/meetings.types.js.map +1 -0
  96. package/dist/meetings/request.js +2 -0
  97. package/dist/meetings/request.js.map +1 -1
  98. package/dist/meetings/util.js +73 -7
  99. package/dist/meetings/util.js.map +1 -1
  100. package/dist/member/index.js +58 -0
  101. package/dist/member/index.js.map +1 -1
  102. package/dist/member/types.js +25 -0
  103. package/dist/member/types.js.map +1 -0
  104. package/dist/member/util.js +132 -25
  105. package/dist/member/util.js.map +1 -1
  106. package/dist/members/collection.js +10 -0
  107. package/dist/members/collection.js.map +1 -1
  108. package/dist/members/index.js +102 -6
  109. package/dist/members/index.js.map +1 -1
  110. package/dist/members/request.js +106 -38
  111. package/dist/members/request.js.map +1 -1
  112. package/dist/members/types.js +15 -0
  113. package/dist/members/types.js.map +1 -0
  114. package/dist/members/util.js +326 -232
  115. package/dist/members/util.js.map +1 -1
  116. package/dist/metrics/constants.js +18 -1
  117. package/dist/metrics/constants.js.map +1 -1
  118. package/dist/metrics/index.js +1 -446
  119. package/dist/metrics/index.js.map +1 -1
  120. package/dist/multistream/mediaRequestManager.js +223 -32
  121. package/dist/multistream/mediaRequestManager.js.map +1 -1
  122. package/dist/multistream/receiveSlot.js +10 -0
  123. package/dist/multistream/receiveSlot.js.map +1 -1
  124. package/dist/multistream/receiveSlotManager.js +39 -36
  125. package/dist/multistream/receiveSlotManager.js.map +1 -1
  126. package/dist/multistream/remoteMedia.js +3 -1
  127. package/dist/multistream/remoteMedia.js.map +1 -1
  128. package/dist/multistream/remoteMediaGroup.js +76 -5
  129. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  130. package/dist/multistream/remoteMediaManager.js +366 -104
  131. package/dist/multistream/remoteMediaManager.js.map +1 -1
  132. package/dist/multistream/sendSlotManager.js +255 -0
  133. package/dist/multistream/sendSlotManager.js.map +1 -0
  134. package/dist/reachability/clusterReachability.js +356 -0
  135. package/dist/reachability/clusterReachability.js.map +1 -0
  136. package/dist/reachability/index.js +263 -390
  137. package/dist/reachability/index.js.map +1 -1
  138. package/dist/reachability/request.js +6 -4
  139. package/dist/reachability/request.js.map +1 -1
  140. package/dist/reachability/util.js +29 -0
  141. package/dist/reachability/util.js.map +1 -0
  142. package/dist/reconnection-manager/index.js +266 -202
  143. package/dist/reconnection-manager/index.js.map +1 -1
  144. package/dist/recording-controller/index.js +21 -2
  145. package/dist/recording-controller/index.js.map +1 -1
  146. package/dist/recording-controller/util.js +9 -8
  147. package/dist/recording-controller/util.js.map +1 -1
  148. package/dist/roap/index.js +51 -28
  149. package/dist/roap/index.js.map +1 -1
  150. package/dist/roap/request.js +48 -64
  151. package/dist/roap/request.js.map +1 -1
  152. package/dist/roap/turnDiscovery.js +220 -70
  153. package/dist/roap/turnDiscovery.js.map +1 -1
  154. package/dist/rtcMetrics/constants.js +12 -0
  155. package/dist/rtcMetrics/constants.js.map +1 -0
  156. package/dist/rtcMetrics/index.js +179 -0
  157. package/dist/rtcMetrics/index.js.map +1 -0
  158. package/dist/statsAnalyzer/index.js +357 -295
  159. package/dist/statsAnalyzer/index.js.map +1 -1
  160. package/dist/statsAnalyzer/mqaUtil.js +296 -156
  161. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  162. package/dist/types/annotation/annotation.types.d.ts +42 -0
  163. package/dist/types/annotation/constants.d.ts +31 -0
  164. package/dist/types/annotation/index.d.ts +117 -0
  165. package/dist/types/breakouts/events.d.ts +8 -0
  166. package/dist/types/breakouts/utils.d.ts +14 -0
  167. package/dist/types/common/errors/no-meeting-info.d.ts +14 -0
  168. package/dist/types/common/errors/reclaim-host-role-errors.d.ts +60 -0
  169. package/dist/types/common/errors/webex-errors.d.ts +25 -1
  170. package/dist/types/common/logs/request.d.ts +2 -0
  171. package/dist/types/common/queue.d.ts +9 -7
  172. package/dist/types/config.d.ts +2 -7
  173. package/dist/types/constants.d.ts +203 -31
  174. package/dist/types/controls-options-manager/enums.d.ts +11 -1
  175. package/dist/types/controls-options-manager/index.d.ts +17 -1
  176. package/dist/types/controls-options-manager/types.d.ts +43 -0
  177. package/dist/types/controls-options-manager/util.d.ts +1 -7
  178. package/dist/types/index.d.ts +6 -5
  179. package/dist/types/interceptors/index.d.ts +2 -0
  180. package/dist/types/interceptors/locusRetry.d.ts +27 -0
  181. package/dist/types/interpretation/collection.d.ts +5 -0
  182. package/dist/types/interpretation/index.d.ts +5 -0
  183. package/dist/types/interpretation/siLanguage.d.ts +5 -0
  184. package/dist/types/locus-info/index.d.ts +57 -4
  185. package/dist/types/locus-info/parser.d.ts +66 -6
  186. package/dist/types/media/index.d.ts +2 -0
  187. package/dist/types/media/properties.d.ts +34 -49
  188. package/dist/types/mediaQualityMetrics/config.d.ts +99 -223
  189. package/dist/types/meeting/in-meeting-actions.d.ts +86 -2
  190. package/dist/types/meeting/index.d.ts +567 -496
  191. package/dist/types/meeting/locusMediaRequest.d.ts +74 -0
  192. package/dist/types/meeting/muteState.d.ts +93 -25
  193. package/dist/types/meeting/request.d.ts +64 -43
  194. package/dist/types/meeting/util.d.ts +117 -1
  195. package/dist/types/meeting-info/index.d.ts +13 -1
  196. package/dist/types/meeting-info/meeting-info-v2.d.ts +31 -1
  197. package/dist/types/meetings/collection.d.ts +17 -0
  198. package/dist/types/meetings/index.d.ts +113 -21
  199. package/dist/types/meetings/meetings.types.d.ts +4 -0
  200. package/dist/types/member/index.d.ts +14 -0
  201. package/dist/types/member/types.d.ts +32 -0
  202. package/dist/types/members/collection.d.ts +5 -0
  203. package/dist/types/members/index.d.ts +35 -2
  204. package/dist/types/members/request.d.ts +73 -9
  205. package/dist/types/members/types.d.ts +25 -0
  206. package/dist/types/members/util.d.ts +214 -1
  207. package/dist/types/metrics/constants.d.ts +17 -0
  208. package/dist/types/metrics/index.d.ts +4 -111
  209. package/dist/types/multistream/mediaRequestManager.d.ts +72 -3
  210. package/dist/types/multistream/receiveSlot.d.ts +7 -3
  211. package/dist/types/multistream/receiveSlotManager.d.ts +14 -4
  212. package/dist/types/multistream/remoteMedia.d.ts +3 -31
  213. package/dist/types/multistream/remoteMediaGroup.d.ts +2 -9
  214. package/dist/types/multistream/remoteMediaManager.d.ts +62 -2
  215. package/dist/types/multistream/sendSlotManager.d.ts +70 -0
  216. package/dist/types/reachability/clusterReachability.d.ts +109 -0
  217. package/dist/types/reachability/index.d.ts +60 -95
  218. package/dist/types/reachability/request.d.ts +3 -1
  219. package/dist/types/reachability/util.d.ts +8 -0
  220. package/dist/types/reconnection-manager/index.d.ts +19 -0
  221. package/dist/types/recording-controller/index.d.ts +15 -1
  222. package/dist/types/recording-controller/util.d.ts +5 -4
  223. package/dist/types/roap/index.d.ts +2 -1
  224. package/dist/types/roap/request.d.ts +9 -8
  225. package/dist/types/roap/turnDiscovery.d.ts +39 -5
  226. package/dist/types/rtcMetrics/constants.d.ts +4 -0
  227. package/dist/types/rtcMetrics/index.d.ts +61 -0
  228. package/dist/types/statsAnalyzer/index.d.ts +34 -12
  229. package/dist/types/statsAnalyzer/mqaUtil.d.ts +28 -4
  230. package/dist/types/webinar/collection.d.ts +16 -0
  231. package/dist/types/webinar/index.d.ts +5 -0
  232. package/dist/webinar/collection.js +44 -0
  233. package/dist/webinar/collection.js.map +1 -0
  234. package/dist/webinar/index.js +69 -0
  235. package/dist/webinar/index.js.map +1 -0
  236. package/package.json +22 -19
  237. package/src/annotation/annotation.types.ts +50 -0
  238. package/src/annotation/constants.ts +36 -0
  239. package/src/annotation/index.ts +328 -0
  240. package/src/breakouts/README.md +35 -11
  241. package/src/breakouts/breakout.ts +67 -9
  242. package/src/breakouts/events.ts +56 -0
  243. package/src/breakouts/index.ts +558 -59
  244. package/src/breakouts/utils.ts +42 -0
  245. package/src/common/errors/no-meeting-info.ts +24 -0
  246. package/src/common/errors/reclaim-host-role-errors.ts +134 -0
  247. package/src/common/errors/webex-errors.ts +44 -2
  248. package/src/common/logs/logger-proxy.ts +1 -1
  249. package/src/common/logs/request.ts +5 -1
  250. package/src/common/queue.ts +22 -8
  251. package/src/config.ts +4 -9
  252. package/src/constants.ts +229 -21
  253. package/src/controls-options-manager/enums.ts +12 -0
  254. package/src/controls-options-manager/index.ts +116 -21
  255. package/src/controls-options-manager/types.ts +59 -0
  256. package/src/controls-options-manager/util.ts +294 -14
  257. package/src/index.ts +44 -0
  258. package/src/interceptors/index.ts +3 -0
  259. package/src/interceptors/locusRetry.ts +67 -0
  260. package/src/interpretation/README.md +60 -0
  261. package/src/interpretation/collection.ts +19 -0
  262. package/src/interpretation/index.ts +349 -0
  263. package/src/interpretation/siLanguage.ts +18 -0
  264. package/src/locus-info/controlsUtils.ts +108 -0
  265. package/src/locus-info/index.ts +417 -59
  266. package/src/locus-info/infoUtils.ts +10 -2
  267. package/src/locus-info/mediaSharesUtils.ts +80 -0
  268. package/src/locus-info/parser.ts +258 -47
  269. package/src/locus-info/selfUtils.ts +81 -5
  270. package/src/media/index.ts +100 -108
  271. package/src/media/properties.ts +88 -117
  272. package/src/mediaQualityMetrics/config.ts +103 -238
  273. package/src/meeting/in-meeting-actions.ts +171 -3
  274. package/src/meeting/index.ts +3411 -2435
  275. package/src/meeting/locusMediaRequest.ts +313 -0
  276. package/src/meeting/muteState.ts +223 -136
  277. package/src/meeting/request.ts +155 -120
  278. package/src/meeting/util.ts +685 -395
  279. package/src/meeting-info/index.ts +81 -8
  280. package/src/meeting-info/meeting-info-v2.ts +170 -14
  281. package/src/meeting-info/util.ts +1 -1
  282. package/src/meeting-info/utilv2.ts +23 -23
  283. package/src/meetings/collection.ts +33 -0
  284. package/src/meetings/index.ts +507 -127
  285. package/src/meetings/meetings.types.ts +12 -0
  286. package/src/meetings/request.ts +2 -0
  287. package/src/meetings/util.ts +81 -12
  288. package/src/member/index.ts +58 -0
  289. package/src/member/types.ts +38 -0
  290. package/src/member/util.ts +141 -25
  291. package/src/members/collection.ts +8 -0
  292. package/src/members/index.ts +134 -8
  293. package/src/members/request.ts +97 -17
  294. package/src/members/types.ts +29 -0
  295. package/src/members/util.ts +333 -240
  296. package/src/metrics/constants.ts +17 -0
  297. package/src/metrics/index.ts +1 -469
  298. package/src/multistream/mediaRequestManager.ts +271 -56
  299. package/src/multistream/receiveSlot.ts +11 -4
  300. package/src/multistream/receiveSlotManager.ts +34 -24
  301. package/src/multistream/remoteMedia.ts +5 -3
  302. package/src/multistream/remoteMediaGroup.ts +78 -0
  303. package/src/multistream/remoteMediaManager.ts +248 -44
  304. package/src/multistream/sendSlotManager.ts +199 -0
  305. package/src/reachability/clusterReachability.ts +320 -0
  306. package/src/reachability/index.ts +229 -346
  307. package/src/reachability/request.ts +8 -4
  308. package/src/reachability/util.ts +24 -0
  309. package/src/reconnection-manager/index.ts +128 -97
  310. package/src/recording-controller/index.ts +20 -3
  311. package/src/recording-controller/util.ts +26 -9
  312. package/src/roap/index.ts +52 -23
  313. package/src/roap/request.ts +48 -67
  314. package/src/roap/turnDiscovery.ts +147 -49
  315. package/src/rtcMetrics/constants.ts +3 -0
  316. package/src/rtcMetrics/index.ts +166 -0
  317. package/src/statsAnalyzer/index.ts +457 -416
  318. package/src/statsAnalyzer/mqaUtil.ts +317 -170
  319. package/src/webinar/collection.ts +31 -0
  320. package/src/webinar/index.ts +62 -0
  321. package/test/integration/spec/converged-space-meetings.js +60 -3
  322. package/test/integration/spec/journey.js +320 -261
  323. package/test/integration/spec/space-meeting.js +76 -3
  324. package/test/unit/spec/annotation/index.ts +418 -0
  325. package/test/unit/spec/breakouts/breakout.ts +118 -28
  326. package/test/unit/spec/breakouts/events.ts +89 -0
  327. package/test/unit/spec/breakouts/index.ts +1349 -114
  328. package/test/unit/spec/breakouts/utils.js +52 -1
  329. package/test/unit/spec/common/queue.js +31 -2
  330. package/test/unit/spec/controls-options-manager/index.js +163 -0
  331. package/test/unit/spec/controls-options-manager/util.js +576 -60
  332. package/test/unit/spec/fixture/locus.js +1 -0
  333. package/test/unit/spec/interceptors/locusRetry.ts +131 -0
  334. package/test/unit/spec/interpretation/collection.ts +15 -0
  335. package/test/unit/spec/interpretation/index.ts +625 -0
  336. package/test/unit/spec/interpretation/siLanguage.ts +28 -0
  337. package/test/unit/spec/locus-info/controlsUtils.js +316 -43
  338. package/test/unit/spec/locus-info/index.js +1363 -37
  339. package/test/unit/spec/locus-info/infoUtils.js +37 -15
  340. package/test/unit/spec/locus-info/lib/SeqCmp.json +16 -0
  341. package/test/unit/spec/locus-info/mediaSharesUtils.ts +41 -0
  342. package/test/unit/spec/locus-info/parser.js +116 -35
  343. package/test/unit/spec/locus-info/selfConstant.js +27 -4
  344. package/test/unit/spec/locus-info/selfUtils.js +208 -17
  345. package/test/unit/spec/media/index.ts +173 -81
  346. package/test/unit/spec/media/properties.ts +2 -2
  347. package/test/unit/spec/meeting/in-meeting-actions.ts +85 -3
  348. package/test/unit/spec/meeting/index.js +6821 -2172
  349. package/test/unit/spec/meeting/locusMediaRequest.ts +442 -0
  350. package/test/unit/spec/meeting/muteState.js +402 -212
  351. package/test/unit/spec/meeting/request.js +473 -54
  352. package/test/unit/spec/meeting/utils.js +773 -67
  353. package/test/unit/spec/meeting-info/index.js +300 -0
  354. package/test/unit/spec/meeting-info/meetinginfov2.js +526 -5
  355. package/test/unit/spec/meeting-info/utilv2.js +21 -0
  356. package/test/unit/spec/meetings/collection.js +26 -0
  357. package/test/unit/spec/meetings/index.js +1415 -213
  358. package/test/unit/spec/meetings/utils.js +229 -2
  359. package/test/unit/spec/member/index.js +61 -6
  360. package/test/unit/spec/member/util.js +510 -34
  361. package/test/unit/spec/members/index.js +432 -1
  362. package/test/unit/spec/members/request.js +206 -27
  363. package/test/unit/spec/members/utils.js +210 -0
  364. package/test/unit/spec/metrics/index.js +1 -50
  365. package/test/unit/spec/multistream/mediaRequestManager.ts +781 -114
  366. package/test/unit/spec/multistream/receiveSlot.ts +9 -1
  367. package/test/unit/spec/multistream/receiveSlotManager.ts +32 -30
  368. package/test/unit/spec/multistream/remoteMedia.ts +2 -0
  369. package/test/unit/spec/multistream/remoteMediaGroup.ts +345 -0
  370. package/test/unit/spec/multistream/remoteMediaManager.ts +525 -0
  371. package/test/unit/spec/multistream/sendSlotManager.ts +274 -0
  372. package/test/unit/spec/reachability/clusterReachability.ts +279 -0
  373. package/test/unit/spec/reachability/index.ts +551 -14
  374. package/test/unit/spec/reachability/request.js +3 -1
  375. package/test/unit/spec/reachability/util.ts +40 -0
  376. package/test/unit/spec/reconnection-manager/index.js +171 -11
  377. package/test/unit/spec/recording-controller/index.js +294 -218
  378. package/test/unit/spec/recording-controller/util.js +223 -96
  379. package/test/unit/spec/roap/index.ts +180 -83
  380. package/test/unit/spec/roap/request.ts +100 -62
  381. package/test/unit/spec/roap/turnDiscovery.ts +388 -96
  382. package/test/unit/spec/rtcMetrics/index.ts +122 -0
  383. package/test/unit/spec/stats-analyzer/index.js +1252 -12
  384. package/test/unit/spec/webinar/collection.ts +13 -0
  385. package/test/unit/spec/webinar/index.ts +60 -0
  386. package/test/utils/integrationTestUtils.js +46 -0
  387. package/test/utils/testUtils.js +0 -57
  388. package/test/utils/webex-test-users.js +12 -4
  389. package/dist/metrics/config.js +0 -289
  390. package/dist/metrics/config.js.map +0 -1
  391. package/dist/types/metrics/config.d.ts +0 -169
  392. package/src/index.js +0 -18
  393. package/src/metrics/config.ts +0 -485
@@ -3,12 +3,14 @@ import sinon from 'sinon';
3
3
  import {cloneDeep} from 'lodash';
4
4
  import {assert} from '@webex/test-helper-chai';
5
5
  import MockWebex from '@webex/test-helper-mock-webex';
6
+ import testUtils from '../../../utils/testUtils';
6
7
  import Meetings from '@webex/plugin-meetings';
7
8
  import LocusInfo from '@webex/plugin-meetings/src/locus-info';
8
9
  import SelfUtils from '@webex/plugin-meetings/src/locus-info/selfUtils';
9
10
  import InfoUtils from '@webex/plugin-meetings/src/locus-info/infoUtils';
10
11
  import EmbeddedAppsUtils from '@webex/plugin-meetings/src/locus-info/embeddedAppsUtils';
11
12
  import LocusDeltaParser from '@webex/plugin-meetings/src/locus-info/parser';
13
+ import Metrics from '@webex/plugin-meetings/src/metrics';
12
14
 
13
15
  import {
14
16
  LOCUSINFO,
@@ -16,6 +18,10 @@ import {
16
18
  LOCUSEVENT,
17
19
  EVENTS,
18
20
  DISPLAY_HINTS,
21
+ _CALL_,
22
+ LOCUS,
23
+ MEETING_STATE,
24
+ _MEETING_,
19
25
  } from '../../../../src/constants';
20
26
 
21
27
  import {self, selfWithInactivity} from './selfConstant';
@@ -33,6 +39,7 @@ describe('plugin-meetings', () => {
33
39
  const locus = {};
34
40
  const meetingId = 'meetingId';
35
41
  let locusInfo;
42
+ let sendBehavioralMetricStub;
36
43
 
37
44
  const webex = new MockWebex({
38
45
  children: {
@@ -59,6 +66,12 @@ describe('plugin-meetings', () => {
59
66
  },
60
67
  },
61
68
  };
69
+
70
+ sendBehavioralMetricStub = sinon.stub(Metrics, 'sendBehavioralMetric');
71
+ });
72
+
73
+ afterEach(() => {
74
+ sinon.restore();
62
75
  });
63
76
 
64
77
  describe('#updateControls', () => {
@@ -66,8 +79,12 @@ describe('plugin-meetings', () => {
66
79
 
67
80
  beforeEach('setup new controls', () => {
68
81
  newControls = {
82
+ disallowUnmute: {enabled: true},
69
83
  lock: {},
70
84
  meetingFull: {},
85
+ muteOnEntry: {enabled: true},
86
+ raiseHand: {enabled: true},
87
+ reactions: {enabled: true, showDisplayNameWithReactions: true},
71
88
  record: {
72
89
  recording: false,
73
90
  paused: false,
@@ -76,12 +93,14 @@ describe('plugin-meetings', () => {
76
93
  modifiedBy: 'George Kittle',
77
94
  },
78
95
  },
79
- shareControl: {},
96
+ shareControl: {control: 'example-value'},
80
97
  transcribe: {},
98
+ viewTheParticipantList: {enabled: true},
81
99
  meetingContainer: {
82
100
  meetingContainerUrl: 'http://new-url.com',
83
101
  },
84
102
  entryExitTone: {enabled: true, mode: 'foo'},
103
+ video: {enabled: true},
85
104
  };
86
105
  });
87
106
 
@@ -95,6 +114,97 @@ describe('plugin-meetings', () => {
95
114
  assert.equal(locusInfo.controls, newControls);
96
115
  });
97
116
 
117
+ it('should trigger the CONTROLS_MUTE_ON_ENTRY_CHANGED event when necessary', () => {
118
+ locusInfo.controls = {};
119
+ locusInfo.emitScoped = sinon.stub();
120
+ locusInfo.updateControls(newControls);
121
+
122
+ assert.calledWith(
123
+ locusInfo.emitScoped,
124
+ {file: 'locus-info', function: 'updateControls'},
125
+ LOCUSINFO.EVENTS.CONTROLS_MUTE_ON_ENTRY_CHANGED,
126
+ {state: newControls.muteOnEntry}
127
+ );
128
+ });
129
+
130
+ it('should trigger the CONTROLS_SHARE_CONTROL_CHANGED event when necessary', () => {
131
+ locusInfo.controls = {};
132
+ locusInfo.emitScoped = sinon.stub();
133
+ locusInfo.updateControls(newControls);
134
+
135
+ assert.calledWith(
136
+ locusInfo.emitScoped,
137
+ {file: 'locus-info', function: 'updateControls'},
138
+ LOCUSINFO.EVENTS.CONTROLS_SHARE_CONTROL_CHANGED,
139
+ {state: newControls.shareControl}
140
+ );
141
+ });
142
+
143
+ it('should trigger the CONTROLS_DISALLOW_UNMUTE_CHANGED event when necessary', () => {
144
+ locusInfo.controls = {};
145
+ locusInfo.emitScoped = sinon.stub();
146
+ locusInfo.updateControls(newControls);
147
+
148
+ assert.calledWith(
149
+ locusInfo.emitScoped,
150
+ {file: 'locus-info', function: 'updateControls'},
151
+ LOCUSINFO.EVENTS.CONTROLS_DISALLOW_UNMUTE_CHANGED,
152
+ {state: newControls.disallowUnmute}
153
+ );
154
+ });
155
+
156
+ it('should trigger the CONTROLS_REACTIONS_CHANGED event when necessary', () => {
157
+ locusInfo.controls = {};
158
+ locusInfo.emitScoped = sinon.stub();
159
+ locusInfo.updateControls(newControls);
160
+
161
+ assert.calledWith(
162
+ locusInfo.emitScoped,
163
+ {file: 'locus-info', function: 'updateControls'},
164
+ LOCUSINFO.EVENTS.CONTROLS_REACTIONS_CHANGED,
165
+ {state: newControls.reactions}
166
+ );
167
+ });
168
+
169
+ it('should trigger the CONTROLS_VIEW_THE_PARTICIPANTS_LIST_CHANGED event when necessary', () => {
170
+ locusInfo.controls = {};
171
+ locusInfo.emitScoped = sinon.stub();
172
+ locusInfo.updateControls(newControls);
173
+
174
+ assert.calledWith(
175
+ locusInfo.emitScoped,
176
+ {file: 'locus-info', function: 'updateControls'},
177
+ LOCUSINFO.EVENTS.CONTROLS_VIEW_THE_PARTICIPANTS_LIST_CHANGED,
178
+ {state: newControls.viewTheParticipantList}
179
+ );
180
+ });
181
+
182
+ it('should trigger the CONTROLS_RAISE_HAND_CHANGED event when necessary', () => {
183
+ locusInfo.controls = {};
184
+ locusInfo.emitScoped = sinon.stub();
185
+ locusInfo.updateControls(newControls);
186
+
187
+ assert.calledWith(
188
+ locusInfo.emitScoped,
189
+ {file: 'locus-info', function: 'updateControls'},
190
+ LOCUSINFO.EVENTS.CONTROLS_RAISE_HAND_CHANGED,
191
+ {state: newControls.raiseHand}
192
+ );
193
+ });
194
+
195
+ it('should trigger the CONTROLS_VIDEO_CHANGED event when necessary', () => {
196
+ locusInfo.controls = {};
197
+ locusInfo.emitScoped = sinon.stub();
198
+ locusInfo.updateControls(newControls);
199
+
200
+ assert.calledWith(
201
+ locusInfo.emitScoped,
202
+ {file: 'locus-info', function: 'updateControls'},
203
+ LOCUSINFO.EVENTS.CONTROLS_VIDEO_CHANGED,
204
+ {state: newControls.video}
205
+ );
206
+ });
207
+
98
208
  it('should not trigger the CONTROLS_RECORDING_UPDATED event', () => {
99
209
  locusInfo.controls = {};
100
210
  locusInfo.emitScoped = sinon.stub();
@@ -279,9 +389,11 @@ describe('plugin-meetings', () => {
279
389
 
280
390
  it('should update the breakout state', () => {
281
391
  locusInfo.emitScoped = sinon.stub();
282
- newControls.breakout = 'new breakout';
392
+ let tmpStub = sinon.stub(SelfUtils, 'getReplacedBreakoutMoveId').returns('breakoutMoveId');
393
+ newControls.breakout = {breakout: {}};
394
+ let selfInfo = {};
283
395
 
284
- locusInfo.updateControls(newControls);
396
+ locusInfo.updateControls(newControls, selfInfo);
285
397
 
286
398
  assert.calledWith(
287
399
  locusInfo.emitScoped,
@@ -291,7 +403,28 @@ describe('plugin-meetings', () => {
291
403
  },
292
404
  LOCUSINFO.EVENTS.CONTROLS_MEETING_BREAKOUT_UPDATED,
293
405
  {
294
- breakout: 'new breakout'
406
+ breakout: newControls.breakout,
407
+ }
408
+ );
409
+ tmpStub.restore();
410
+ });
411
+
412
+ it('should update the interpretation state', () => {
413
+ locusInfo.emitScoped = sinon.stub();
414
+ newControls.interpretation = {siLanguages: [{languageCode: 20, languageName: 'en'}]};
415
+ let selfInfo = {};
416
+
417
+ locusInfo.updateControls(newControls, selfInfo);
418
+
419
+ assert.calledWith(
420
+ locusInfo.emitScoped,
421
+ {
422
+ file: 'locus-info',
423
+ function: 'updateControls',
424
+ },
425
+ LOCUSINFO.EVENTS.CONTROLS_MEETING_INTERPRETATION_UPDATED,
426
+ {
427
+ interpretation: newControls.interpretation,
295
428
  }
296
429
  );
297
430
  });
@@ -417,6 +550,39 @@ describe('plugin-meetings', () => {
417
550
  assert.notEqual(x.args[1], LOCUSINFO.EVENTS.CONTROLS_ENTRY_EXIT_TONE_UPDATED);
418
551
  });
419
552
  });
553
+
554
+ it('should update videoEnabled when changed', () => {
555
+ locusInfo.controls = {};
556
+
557
+ locusInfo.emitScoped = sinon.stub();
558
+ locusInfo.updateControls(newControls);
559
+
560
+ assert.calledWith(
561
+ locusInfo.emitScoped,
562
+ {
563
+ file: 'locus-info',
564
+ function: 'updateControls',
565
+ },
566
+ LOCUSINFO.EVENTS.SELF_REMOTE_VIDEO_MUTE_STATUS_UPDATED,
567
+ {unmuteAllowed: true}
568
+ );
569
+
570
+ assert.equal(mockMeeting.unmuteVideoAllowed, true);
571
+ });
572
+
573
+ it('should not update videoEnabled when unchanged', () => {
574
+ locusInfo.controls = {videoEnabled: true};
575
+
576
+ locusInfo.emitScoped = sinon.stub();
577
+ locusInfo.updateControls(newControls);
578
+
579
+ locusInfo.emitScoped.getCalls().forEach((x) => {
580
+ // check that no calls in emitScoped are for SELF_REMOTE_VIDEO_MUTE_STATUS_UPDATED
581
+ assert.notEqual(x.args[1], LOCUSINFO.EVENTS.SELF_REMOTE_VIDEO_MUTE_STATUS_UPDATED);
582
+ });
583
+
584
+ assert.equal(mockMeeting.unmuteVideoAllowed, undefined);
585
+ });
420
586
  });
421
587
 
422
588
  describe('#updateParticipants()', () => {
@@ -470,6 +636,7 @@ describe('plugin-meetings', () => {
470
636
  selfIdentity: '123',
471
637
  selfId: '2',
472
638
  hostId: '3',
639
+ isReplace: undefined,
473
640
  }
474
641
  );
475
642
  // note: in a real use case, recordingId, selfId, and hostId would all be the same
@@ -477,6 +644,43 @@ describe('plugin-meetings', () => {
477
644
  // are being correctly grabbed from locusInfo.parsedLocus within updateParticipants
478
645
  });
479
646
 
647
+ it('should call with breakout control info', () => {
648
+ locusInfo.parsedLocus = {
649
+ controls: {
650
+ record: {
651
+ modifiedBy: '1',
652
+ },
653
+ },
654
+ self: {
655
+ selfIdentity: '123',
656
+ selfId: '2',
657
+ },
658
+ host: {
659
+ hostId: '3',
660
+ },
661
+ };
662
+
663
+ locusInfo.emitScoped = sinon.stub();
664
+ locusInfo.updateParticipants({}, true);
665
+
666
+ assert.calledWith(
667
+ locusInfo.emitScoped,
668
+ {
669
+ file: 'locus-info',
670
+ function: 'updateParticipants',
671
+ },
672
+ EVENTS.LOCUS_INFO_UPDATE_PARTICIPANTS,
673
+ {
674
+ participants: {},
675
+ recordingId: '1',
676
+ selfIdentity: '123',
677
+ selfId: '2',
678
+ hostId: '3',
679
+ isReplace: true,
680
+ }
681
+ );
682
+ });
683
+
480
684
  it('should update the deltaParticipants object', () => {
481
685
  const prev = locusInfo.deltaParticipants;
482
686
 
@@ -683,6 +887,83 @@ describe('plugin-meetings', () => {
683
887
  );
684
888
  });
685
889
 
890
+ describe('SELF_REMOTE_VIDEO_MUTE_STATUS_UPDATED', () => {
891
+ it('should emit event when video muted on entry', () => {
892
+ // usually "previous self" is just undefined when we get first self from locus
893
+ locusInfo.self = undefined;
894
+ const selfWithMutedByOthers = cloneDeep(self);
895
+
896
+ // remoteVideoMuted
897
+ selfWithMutedByOthers.controls.video.muted = true;
898
+
899
+ locusInfo.webex.internal.device.url = self.deviceUrl;
900
+ locusInfo.emitScoped = sinon.stub();
901
+ locusInfo.updateSelf(selfWithMutedByOthers, []);
902
+
903
+ assert.calledWith(
904
+ locusInfo.emitScoped,
905
+ {
906
+ file: 'locus-info',
907
+ function: 'updateSelf',
908
+ },
909
+ LOCUSINFO.EVENTS.SELF_REMOTE_VIDEO_MUTE_STATUS_UPDATED,
910
+ {muted: true}
911
+ );
912
+
913
+ // but sometimes "previous self" is defined, but without controls.audio.muted, so we test this here:
914
+ locusInfo.self = cloneDeep(self);
915
+ locusInfo.self.controls.video = {};
916
+
917
+ locusInfo.updateSelf(selfWithMutedByOthers, []);
918
+ assert.calledWith(
919
+ locusInfo.emitScoped,
920
+ {
921
+ file: 'locus-info',
922
+ function: 'updateSelf',
923
+ },
924
+ LOCUSINFO.EVENTS.SELF_REMOTE_VIDEO_MUTE_STATUS_UPDATED,
925
+ {muted: true}
926
+ );
927
+ });
928
+
929
+ it('should not emit event when not muted on entry', () => {
930
+ locusInfo.self = undefined;
931
+ const selfWithMutedByOthersFalse = cloneDeep(self);
932
+
933
+ selfWithMutedByOthersFalse.controls.video.muted = false;
934
+
935
+ locusInfo.webex.internal.device.url = self.deviceUrl;
936
+ locusInfo.emitScoped = sinon.stub();
937
+ locusInfo.updateSelf(selfWithMutedByOthersFalse, []);
938
+
939
+ // we might get some calls to emitScoped, but we need to check that none of them are for SELF_REMOTE_VIDEO_MUTE_STATUS_UPDATED
940
+ locusInfo.emitScoped.getCalls().forEach((x) => {
941
+ assert.notEqual(x.args[1], LOCUSINFO.EVENTS.SELF_REMOTE_VIDEO_MUTE_STATUS_UPDATED);
942
+ });
943
+ });
944
+
945
+ it('should emit event when remoteVideoMuted changed', () => {
946
+ locusInfo.self = self;
947
+ const selfWithMutedByOthers = cloneDeep(self);
948
+
949
+ selfWithMutedByOthers.controls.video.muted = true;
950
+
951
+ locusInfo.webex.internal.device.url = self.deviceUrl;
952
+ locusInfo.emitScoped = sinon.stub();
953
+ locusInfo.updateSelf(selfWithMutedByOthers, []);
954
+
955
+ assert.calledWith(
956
+ locusInfo.emitScoped,
957
+ {
958
+ file: 'locus-info',
959
+ function: 'updateSelf',
960
+ },
961
+ LOCUSINFO.EVENTS.SELF_REMOTE_VIDEO_MUTE_STATUS_UPDATED,
962
+ {muted: true}
963
+ );
964
+ });
965
+ });
966
+
686
967
  it('should trigger SELF_MEETING_BREAKOUTS_CHANGED when breakouts changed', () => {
687
968
  locusInfo.self = self;
688
969
  const selfWithBreakoutsChanged = cloneDeep(self);
@@ -701,19 +982,23 @@ describe('plugin-meetings', () => {
701
982
  LOCUSINFO.EVENTS.SELF_MEETING_BREAKOUTS_CHANGED,
702
983
  {
703
984
  breakoutSessions: {
704
- active: [{
705
- name: 'new name',
706
- groupId: '0e73abb8-5584-49d8-be8d-806d2a8247ca',
707
- sessionId: '1cf41ab1-2e57-4d95-b7e9-5613acddfb0f',
708
- sessionType: 'BREAKOUT'
709
- }],
710
- allowed: [{
711
- name: 'Breakout session 2',
712
- groupId: '0e73abb8-5584-49d8-be8d-806d2a8247ca',
713
- sessionId: '1cf41ab1-2e57-4d95-b7e9-5613acddfb0f',
714
- sessionType: 'BREAKOUT'
715
- }]
716
- }
985
+ active: [
986
+ {
987
+ name: 'new name',
988
+ groupId: '0e73abb8-5584-49d8-be8d-806d2a8247ca',
989
+ sessionId: '1cf41ab1-2e57-4d95-b7e9-5613acddfb0f',
990
+ sessionType: 'BREAKOUT',
991
+ },
992
+ ],
993
+ allowed: [
994
+ {
995
+ name: 'Breakout session 2',
996
+ groupId: '0e73abb8-5584-49d8-be8d-806d2a8247ca',
997
+ sessionId: '1cf41ab1-2e57-4d95-b7e9-5613acddfb0f',
998
+ sessionType: 'BREAKOUT',
999
+ },
1000
+ ],
1001
+ },
717
1002
  }
718
1003
  );
719
1004
  });
@@ -789,6 +1074,8 @@ describe('plugin-meetings', () => {
789
1074
  const selfWithRequestedToUnmute = cloneDeep(self);
790
1075
 
791
1076
  selfWithRequestedToUnmute.controls.audio.requestedToUnmute = true;
1077
+ selfWithRequestedToUnmute.controls.audio.lastModifiedRequestedToUnmute =
1078
+ '2023-06-16T19:25:04.369Z';
792
1079
 
793
1080
  locusInfo.webex.internal.device.url = self.deviceUrl;
794
1081
  locusInfo.emitScoped = sinon.stub();
@@ -943,6 +1230,88 @@ describe('plugin-meetings', () => {
943
1230
  {isSharingBlocked: true}
944
1231
  );
945
1232
  });
1233
+
1234
+ it('should trigger SELF_ROLES_CHANGED if self roles changed', () => {
1235
+ locusInfo.self = self;
1236
+ locusInfo.emitScoped = sinon.stub();
1237
+ const sampleNewSelf = cloneDeep(self);
1238
+ sampleNewSelf.controls.role.roles = [{type: 'COHOST', hasRole: true}];
1239
+
1240
+ locusInfo.updateSelf(sampleNewSelf, []);
1241
+
1242
+ assert.calledWith(
1243
+ locusInfo.emitScoped,
1244
+ {
1245
+ file: 'locus-info',
1246
+ function: 'updateSelf',
1247
+ },
1248
+ LOCUSINFO.EVENTS.SELF_ROLES_CHANGED,
1249
+ {oldRoles: ['PRESENTER'], newRoles: ['COHOST']}
1250
+ );
1251
+ });
1252
+
1253
+ it('should not trigger SELF_ROLES_CHANGED if self roles not changed', () => {
1254
+ locusInfo.self = self;
1255
+ locusInfo.emitScoped = sinon.stub();
1256
+ const sampleNewSelf = cloneDeep(self);
1257
+ sampleNewSelf.controls.role.roles = [{type: 'PRESENTER', hasRole: true}];
1258
+
1259
+ locusInfo.updateSelf(sampleNewSelf, []);
1260
+
1261
+ assert.neverCalledWith(
1262
+ locusInfo.emitScoped,
1263
+ {
1264
+ file: 'locus-info',
1265
+ function: 'updateSelf',
1266
+ },
1267
+ LOCUSINFO.EVENTS.SELF_ROLES_CHANGED,
1268
+ {oldRoles: ['PRESENTER'], newRoles: ['PRESENTER']}
1269
+ );
1270
+ });
1271
+
1272
+ it('should trigger SELF_MEETING_INTERPRETATION_CHANGED if self interpretation info changed', () => {
1273
+ locusInfo.self = self;
1274
+ locusInfo.emitScoped = sinon.stub();
1275
+ const sampleNewSelf = cloneDeep(self);
1276
+ sampleNewSelf.controls.interpretation.targetLanguage = 'it';
1277
+
1278
+ locusInfo.updateSelf(sampleNewSelf, []);
1279
+
1280
+ assert.calledWith(
1281
+ locusInfo.emitScoped,
1282
+ {
1283
+ file: 'locus-info',
1284
+ function: 'updateSelf',
1285
+ },
1286
+ LOCUSINFO.EVENTS.SELF_MEETING_INTERPRETATION_CHANGED,
1287
+ {
1288
+ interpretation: sampleNewSelf.controls.interpretation,
1289
+ selfParticipantId: self.id,
1290
+ }
1291
+ );
1292
+ });
1293
+
1294
+ it('should not trigger SELF_MEETING_INTERPRETATION_CHANGED if self interpretation info not changed', () => {
1295
+ locusInfo.self = self;
1296
+ locusInfo.emitScoped = sinon.stub();
1297
+ const sampleNewSelf = cloneDeep(self);
1298
+ sampleNewSelf.controls.interpretation.targetLanguage = 'cn'; // same with previous one
1299
+
1300
+ locusInfo.updateSelf(sampleNewSelf, []);
1301
+
1302
+ assert.neverCalledWith(
1303
+ locusInfo.emitScoped,
1304
+ {
1305
+ file: 'locus-info',
1306
+ function: 'updateSelf',
1307
+ },
1308
+ LOCUSINFO.EVENTS.SELF_MEETING_INTERPRETATION_CHANGED,
1309
+ {
1310
+ interpretation: sampleNewSelf.controls.interpretation,
1311
+ selfParticipantId: self.id,
1312
+ }
1313
+ );
1314
+ });
946
1315
  });
947
1316
 
948
1317
  describe('#updateMeetingInfo', () => {
@@ -1008,7 +1377,26 @@ describe('plugin-meetings', () => {
1008
1377
  );
1009
1378
  });
1010
1379
 
1011
- const checkMeetingInfoUpdatedCalled = (expected) => {
1380
+ const checkMeetingInfoUpdatedCalled = (expected, payload) => {
1381
+ const expectedArgs = [
1382
+ locusInfo.emitScoped,
1383
+ {
1384
+ file: 'locus-info',
1385
+ function: 'updateMeetingInfo',
1386
+ },
1387
+ LOCUSINFO.EVENTS.MEETING_INFO_UPDATED,
1388
+ payload
1389
+ ];
1390
+
1391
+ if (expected) {
1392
+ assert.calledWith(...expectedArgs);
1393
+ } else {
1394
+ assert.neverCalledWith(...expectedArgs);
1395
+ }
1396
+ locusInfo.emitScoped.resetHistory();
1397
+ };
1398
+
1399
+ const checkMeetingInfoUpdatedCalledForRoles = (expected, payload) => {
1012
1400
  const expectedArgs = [
1013
1401
  locusInfo.emitScoped,
1014
1402
  {
@@ -1016,7 +1404,7 @@ describe('plugin-meetings', () => {
1016
1404
  function: 'updateMeetingInfo',
1017
1405
  },
1018
1406
  LOCUSINFO.EVENTS.MEETING_INFO_UPDATED,
1019
- {info: locusInfo.parsedLocus.info, self},
1407
+ payload
1020
1408
  ];
1021
1409
 
1022
1410
  if (expected) {
@@ -1027,32 +1415,120 @@ describe('plugin-meetings', () => {
1027
1415
  locusInfo.emitScoped.resetHistory();
1028
1416
  };
1029
1417
 
1030
- it('emits MEETING_INFO_UPDATED if the info changes', () => {
1418
+ it('emits MEETING_INFO_UPDATED and updates the meeting if the info changes', () => {
1031
1419
  const initialInfo = cloneDeep(meetingInfo);
1032
1420
 
1033
- locusInfo.emitScoped = sinon.stub();
1421
+ let expectedMeeting;
1422
+
1423
+ /*
1424
+ When the event is triggered, it is required that the meeting has already
1425
+ been updated. This is why the meeting is being checked within the stubbed event emitter
1426
+ */
1427
+ sinon.stub(locusInfo, 'emitScoped').callsFake(() => {
1428
+ assert.deepEqual(mockMeeting, expectedMeeting);
1429
+ })
1430
+
1034
1431
 
1035
1432
  // set the info initially as locusInfo.info starts as undefined
1433
+ expectedMeeting = {
1434
+ coHost: {
1435
+ LOWER_SOMEONE_ELSES_HAND: true,
1436
+ },
1437
+ isLocked: false,
1438
+ isUnlocked: true,
1439
+ moderator: {
1440
+ LOWER_SOMEONE_ELSES_HAND: true,
1441
+ },
1442
+ policy: {
1443
+ LOCK_STATUS_UNLOCKED: true,
1444
+ ROSTER_IN_MEETING: true,
1445
+ },
1446
+ userDisplayHints: ['ROSTER_IN_MEETING', 'LOCK_STATUS_UNLOCKED'],
1447
+ };
1036
1448
  locusInfo.updateMeetingInfo(initialInfo, self);
1037
1449
 
1038
1450
  // since it was initially undefined, this should trigger the event
1039
- checkMeetingInfoUpdatedCalled(true);
1451
+
1452
+ checkMeetingInfoUpdatedCalled(true, {isInitializing: false});
1040
1453
 
1041
1454
  const newInfo = cloneDeep(meetingInfo);
1042
1455
 
1043
1456
  newInfo.displayHints.coHost = [DISPLAY_HINTS.LOCK_CONTROL_LOCK];
1044
1457
 
1045
1458
  // Updating with different info should trigger the event
1459
+ expectedMeeting = {
1460
+ coHost: {
1461
+ LOWER_SOMEONE_ELSES_HAND: true,
1462
+ LOCK_CONTROL_LOCK: true,
1463
+ },
1464
+ isLocked: false,
1465
+ isUnlocked: true,
1466
+ moderator: {
1467
+ LOWER_SOMEONE_ELSES_HAND: true,
1468
+ },
1469
+ policy: {
1470
+ LOCK_STATUS_UNLOCKED: true,
1471
+ ROSTER_IN_MEETING: true,
1472
+ },
1473
+ userDisplayHints: ['ROSTER_IN_MEETING', 'LOCK_STATUS_UNLOCKED'],
1474
+ };
1046
1475
  locusInfo.updateMeetingInfo(newInfo, self);
1047
1476
 
1048
- checkMeetingInfoUpdatedCalled(true);
1477
+ checkMeetingInfoUpdatedCalled(true, {isInitializing: false});
1049
1478
 
1050
1479
  // update it with the same info
1480
+ expectedMeeting = {
1481
+ coHost: {
1482
+ LOWER_SOMEONE_ELSES_HAND: true,
1483
+ LOCK_CONTROL_LOCK: true,
1484
+ },
1485
+ isLocked: false,
1486
+ isUnlocked: true,
1487
+ moderator: {
1488
+ LOWER_SOMEONE_ELSES_HAND: true,
1489
+ },
1490
+ policy: {
1491
+ LOCK_STATUS_UNLOCKED: true,
1492
+ ROSTER_IN_MEETING: true,
1493
+ },
1494
+ userDisplayHints: ['ROSTER_IN_MEETING', 'LOCK_STATUS_UNLOCKED'],
1495
+ };
1051
1496
  locusInfo.updateMeetingInfo(newInfo, self);
1052
1497
 
1053
1498
  // since the info is the same it should not call trigger the event
1054
- checkMeetingInfoUpdatedCalled(false);
1055
- });
1499
+ checkMeetingInfoUpdatedCalled(false, {isInitializing: false});
1500
+
1501
+ // update it with the same info, but roles changed
1502
+ const updateSelf = cloneDeep(self);
1503
+ updateSelf?.controls?.role?.roles.push({
1504
+ type: 'COHOST',
1505
+ hasRole: true,
1506
+ });
1507
+ expectedMeeting = {
1508
+ coHost: {
1509
+ LOWER_SOMEONE_ELSES_HAND: true,
1510
+ LOCK_CONTROL_LOCK: true,
1511
+ },
1512
+ isLocked: false,
1513
+ isUnlocked: true,
1514
+ moderator: {
1515
+ LOWER_SOMEONE_ELSES_HAND: true,
1516
+ },
1517
+ policy: {
1518
+ LOCK_STATUS_UNLOCKED: true,
1519
+ ROSTER_IN_MEETING: true,
1520
+ },
1521
+ userDisplayHints: [
1522
+ 'ROSTER_IN_MEETING',
1523
+ 'LOCK_STATUS_UNLOCKED',
1524
+ 'LOCK_CONTROL_LOCK',
1525
+ 'LOWER_SOMEONE_ELSES_HAND',
1526
+ ],
1527
+ };
1528
+ locusInfo.updateMeetingInfo(newInfo, updateSelf);
1529
+ // since the info is the same but roles changed, it should call trigger the event
1530
+ checkMeetingInfoUpdatedCalledForRoles(true, {isInitializing: false});
1531
+ });
1056
1532
 
1057
1533
  it('gets roles from self if available', () => {
1058
1534
  const initialInfo = cloneDeep(meetingInfo);
@@ -1072,12 +1548,17 @@ describe('plugin-meetings', () => {
1072
1548
  roles: ['MODERATOR', 'COHOST'],
1073
1549
  };
1074
1550
 
1551
+ sinon.stub(locusInfo, 'emitScoped');
1552
+
1075
1553
  const parsedLocusInfo = cloneDeep(locusInfo.parsedLocus.info);
1076
1554
 
1077
1555
  locusInfo.updateMeetingInfo(initialInfo);
1078
1556
  assert.calledWith(isJoinedSpy, locusInfo.parsedLocus.self);
1079
1557
  assert.neverCalledWith(getRolesSpy, self);
1080
1558
  assert.calledWith(getInfosSpy, parsedLocusInfo, initialInfo, ['MODERATOR', 'COHOST']);
1559
+
1560
+ // since self is not passed to updateMeetingInfo, MEETING_INFO_UPDATED should be triggered with isIntializing: true
1561
+ checkMeetingInfoUpdatedCalledForRoles(true, {isInitializing: true});
1081
1562
  });
1082
1563
  });
1083
1564
 
@@ -1157,6 +1638,8 @@ describe('plugin-meetings', () => {
1157
1638
  fakeLocus = {
1158
1639
  meeting: true,
1159
1640
  participants: true,
1641
+ url: 'newLocusUrl',
1642
+ syncUrl: 'newSyncUrl',
1160
1643
  };
1161
1644
  });
1162
1645
 
@@ -1205,8 +1688,8 @@ describe('plugin-meetings', () => {
1205
1688
  const newLocus = {
1206
1689
  self: {
1207
1690
  reason: 'MOVED',
1208
- state: 'LEFT'
1209
- }
1691
+ state: 'LEFT',
1692
+ },
1210
1693
  };
1211
1694
 
1212
1695
  locusInfo.updateControls = sinon.stub();
@@ -1258,12 +1741,39 @@ describe('plugin-meetings', () => {
1258
1741
  sandbox.stub(locusInfo, 'updateParticipants');
1259
1742
  sandbox.stub(locusInfo, 'isMeetingActive');
1260
1743
  sandbox.stub(locusInfo, 'handleOneOnOneEvent');
1744
+ sandbox.stub(locusParser, 'isNewFullLocus').returns(true);
1261
1745
 
1262
1746
  locusInfo.onFullLocus(fakeLocus, eventType);
1263
1747
 
1264
1748
  assert.equal(fakeLocus, locusParser.workingCopy);
1265
1749
  });
1266
1750
 
1751
+ it('onFullLocus() does not do anything if the incoming full locus DTO is old', () => {
1752
+ const eventType = 'fakeEvent';
1753
+
1754
+ locusParser.workingCopy = {};
1755
+
1756
+ const oldWorkingCopy = locusParser.workingCopy;
1757
+
1758
+ const spies = [
1759
+ sandbox.stub(locusInfo, 'updateParticipantDeltas'),
1760
+ sandbox.stub(locusInfo, 'updateLocusInfo'),
1761
+ sandbox.stub(locusInfo, 'updateParticipants'),
1762
+ sandbox.stub(locusInfo, 'isMeetingActive'),
1763
+ sandbox.stub(locusInfo, 'handleOneOnOneEvent'),
1764
+ ];
1765
+
1766
+ sandbox.stub(locusParser, 'isNewFullLocus').returns(false);
1767
+
1768
+ locusInfo.onFullLocus(fakeLocus, eventType);
1769
+
1770
+ spies.forEach((spy) => {
1771
+ assert.notCalled(spy);
1772
+ });
1773
+
1774
+ assert.equal(oldWorkingCopy, locusParser.workingCopy);
1775
+ });
1776
+
1267
1777
  it('onDeltaAction applies locus delta data to meeting', () => {
1268
1778
  const action = 'fake action';
1269
1779
  const parsedLoci = 'fake loci';
@@ -1290,33 +1800,82 @@ describe('plugin-meetings', () => {
1290
1800
  assert.calledWith(meeting.locusInfo.onDeltaLocus, fakeLocus);
1291
1801
  });
1292
1802
 
1293
- it('applyLocusDeltaData gets full locus on DESYNC action', () => {
1803
+ it('applyLocusDeltaData gets delta locus on DESYNC action if we have a syncUrl', () => {
1804
+ const {DESYNC} = LocusDeltaParser.loci;
1805
+ const fakeDeltaLocus = {id: 'fake delta locus'};
1806
+ const meeting = {
1807
+ meetingRequest: {
1808
+ getLocusDTO: sandbox.stub().resolves({body: fakeDeltaLocus}),
1809
+ },
1810
+ locusInfo: {
1811
+ handleLocusDelta: sandbox.stub(),
1812
+ },
1813
+ locusUrl: 'oldLocusUrl',
1814
+ };
1815
+
1816
+ locusInfo.locusParser.workingCopy = {
1817
+ syncUrl: 'oldSyncUrl',
1818
+ };
1819
+
1820
+ // Since we have a promise inside a function we want to test that's not returned,
1821
+ // we will wait and stub it's last function to resolve this waiting promise.
1822
+ // Also ensures .handleLocusDelta() is called before .resume()
1823
+ return new Promise((resolve) => {
1824
+ locusInfo.locusParser.resume = sandbox.stub().callsFake(() => resolve());
1825
+ locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
1826
+ }).then(() => {
1827
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'oldSyncUrl'});
1828
+
1829
+ assert.calledOnceWithExactly(meeting.locusInfo.handleLocusDelta, fakeDeltaLocus, meeting);
1830
+ assert.calledOnce(locusInfo.locusParser.resume);
1831
+ });
1832
+ });
1833
+
1834
+ it('applyLocusDeltaData gets delta locus on DESYNC action if we have a syncUrl (empty response body)', () => {
1294
1835
  const {DESYNC} = LocusDeltaParser.loci;
1295
1836
  const meeting = {
1296
1837
  meetingRequest: {
1297
- getFullLocus: sandbox.stub().resolves(true),
1838
+ getLocusDTO: sandbox.stub().resolves({body: {}}),
1298
1839
  },
1299
1840
  locusInfo: {
1841
+ handleLocusDelta: sandbox.stub(),
1300
1842
  onFullLocus: sandbox.stub(),
1301
1843
  },
1844
+ locusUrl: 'oldLocusUrl',
1302
1845
  };
1303
1846
 
1304
- locusInfo.locusParser.resume = sandbox.stub();
1305
- locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
1847
+ locusInfo.locusParser.workingCopy = {
1848
+ syncUrl: 'oldSyncUrl',
1849
+ };
1850
+
1851
+ // Since we have a promise inside a function we want to test that's not returned,
1852
+ // we will wait and stub it's last function to resolve this waiting promise.
1853
+ return new Promise((resolve) => {
1854
+ locusInfo.locusParser.resume = sandbox.stub().callsFake(() => resolve());
1855
+ locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
1856
+ }).then(() => {
1857
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'oldSyncUrl'});
1306
1858
 
1307
- assert.calledOnce(meeting.meetingRequest.getFullLocus);
1859
+ assert.notCalled(meeting.locusInfo.handleLocusDelta);
1860
+ assert.notCalled(meeting.locusInfo.onFullLocus);
1861
+ assert.calledOnce(locusInfo.locusParser.resume);
1862
+ });
1308
1863
  });
1309
1864
 
1310
- it('getFullLocus handles DESYNC action correctly', () => {
1865
+ it('applyLocusDeltaData gets full locus on DESYNC action if we do not have a syncUrl', () => {
1311
1866
  const {DESYNC} = LocusDeltaParser.loci;
1867
+ const fakeFullLocusDto = {id: 'fake full locus dto'};
1312
1868
  const meeting = {
1313
1869
  meetingRequest: {
1314
- getFullLocus: sandbox.stub().resolves({body: true}),
1870
+ getLocusDTO: sandbox.stub().resolves({body: fakeFullLocusDto}),
1871
+ },
1872
+ locusInfo: {
1873
+ onFullLocus: sandbox.stub(),
1315
1874
  },
1316
- locusInfo,
1875
+ locusUrl: 'oldLocusUrl',
1317
1876
  };
1318
1877
 
1319
- locusInfo.onFullLocus = sandbox.stub();
1878
+ locusInfo.locusParser.workingCopy = {}; // no syncUrl
1320
1879
 
1321
1880
  // Since we have a promise inside a function we want to test that's not returned,
1322
1881
  // we will wait and stub it's last function to resolve this waiting promise.
@@ -1325,10 +1884,378 @@ describe('plugin-meetings', () => {
1325
1884
  locusInfo.locusParser.resume = sandbox.stub().callsFake(() => resolve());
1326
1885
  locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
1327
1886
  }).then(() => {
1328
- assert.calledOnce(meeting.locusInfo.onFullLocus);
1887
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'oldLocusUrl'});
1888
+
1889
+ assert.calledOnceWithExactly(meeting.locusInfo.onFullLocus, fakeFullLocusDto);
1329
1890
  assert.calledOnce(locusInfo.locusParser.resume);
1330
1891
  });
1331
1892
  });
1893
+
1894
+ it('applyLocusDeltaData handles LOCUS_URL_CHANGED action correctly', () => {
1895
+ const {LOCUS_URL_CHANGED} = LocusDeltaParser.loci;
1896
+ const fakeDeltaLocus = {id: 'fake delta locus'};
1897
+ const meeting = {
1898
+ meetingRequest: {
1899
+ getLocusDTO: sandbox.stub().resolves({body: fakeDeltaLocus}),
1900
+ },
1901
+ locusInfo: {
1902
+ handleLocusDelta: sandbox.stub(),
1903
+ },
1904
+ locusUrl: 'current locus url',
1905
+ };
1906
+
1907
+ locusInfo.locusParser.workingCopy = {
1908
+ syncUrl: 'current sync url',
1909
+ };
1910
+
1911
+ locusInfo.applyLocusDeltaData(LOCUS_URL_CHANGED, fakeLocus, meeting);
1912
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'current sync url'});
1913
+ });
1914
+
1915
+ describe('edge cases for sync failing', () => {
1916
+ const {DESYNC} = LocusDeltaParser.loci;
1917
+ const fakeFullLocusDto = {id: 'fake full locus dto'};
1918
+ let meeting;
1919
+
1920
+ beforeEach(() => {
1921
+ sinon.stub(locusInfo.locusParser, 'resume');
1922
+ sinon.stub(webex.meetings, 'destroy');
1923
+
1924
+ meeting = {
1925
+ meetingRequest: {
1926
+ getLocusDTO: sandbox.stub(),
1927
+ },
1928
+ locusInfo: {
1929
+ handleLocusDelta: sandbox.stub(),
1930
+ onFullLocus: sandbox.stub(),
1931
+ },
1932
+ locusUrl: 'fullSyncUrl',
1933
+ };
1934
+
1935
+ locusInfo.locusParser.workingCopy = {
1936
+ syncUrl: 'deltaSyncUrl',
1937
+ };
1938
+ });
1939
+
1940
+ it('applyLocusDeltaData gets full locus on DESYNC action if we do not have a syncUrl and destroys the meeting if that fails', () => {
1941
+ meeting.meetingRequest.getLocusDTO.rejects(new Error('fake error'));
1942
+
1943
+ locusInfo.locusParser.workingCopy = {}; // no syncUrl
1944
+
1945
+ // Since we have a promise inside a function we want to test that's not returned,
1946
+ // we will wait and stub it's last function to resolve this waiting promise.
1947
+ return new Promise((resolve) => {
1948
+ webex.meetings.destroy.callsFake(() => resolve());
1949
+ locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
1950
+ }).then(() => {
1951
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'fullSyncUrl'});
1952
+
1953
+ assert.notCalled(meeting.locusInfo.handleLocusDelta);
1954
+ assert.notCalled(meeting.locusInfo.onFullLocus);
1955
+ assert.notCalled(locusInfo.locusParser.resume);
1956
+
1957
+ assert.calledOnceWithExactly(webex.meetings.destroy, meeting, 'LOCUS_DTO_SYNC_FAILED');
1958
+ });
1959
+ });
1960
+
1961
+ it('applyLocusDeltaData first tries a delta sync on DESYNC action and if that fails, does a full locus sync', () => {
1962
+ meeting.meetingRequest.getLocusDTO.onCall(0).rejects(new Error('fake error'));
1963
+ meeting.meetingRequest.getLocusDTO.onCall(1).resolves({body: fakeFullLocusDto});
1964
+
1965
+ // Since we have a promise inside a function we want to test that's not returned,
1966
+ // we will wait and stub it's last function to resolve this waiting promise.
1967
+ return new Promise((resolve) => {
1968
+ locusInfo.locusParser.resume.callsFake(() => resolve());
1969
+ locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
1970
+ }).then(() => {
1971
+ assert.calledTwice(meeting.meetingRequest.getLocusDTO);
1972
+
1973
+ assert.deepEqual(meeting.meetingRequest.getLocusDTO.getCalls()[0].args, [{url: 'deltaSyncUrl'}]);
1974
+ assert.deepEqual(meeting.meetingRequest.getLocusDTO.getCalls()[1].args, [{url: 'fullSyncUrl'}]);
1975
+
1976
+ assert.calledWith(sendBehavioralMetricStub, 'js_sdk_locus_delta_sync_failed', {
1977
+ correlationId: meeting.correlationId,
1978
+ url: 'deltaSyncUrl',
1979
+ reason: 'fake error',
1980
+ errorName: 'Error',
1981
+ stack: sinon.match.any,
1982
+ code: sinon.match.any,
1983
+ });
1984
+
1985
+ assert.notCalled(meeting.locusInfo.handleLocusDelta);
1986
+ assert.calledOnceWithExactly(meeting.locusInfo.onFullLocus, fakeFullLocusDto);
1987
+ assert.calledOnce(locusInfo.locusParser.resume);
1988
+ });
1989
+ });
1990
+
1991
+ it('applyLocusDeltaData destroys the meeting if both delta sync and full sync fail', () => {
1992
+ meeting.meetingRequest.getLocusDTO.rejects(new Error('fake error'));
1993
+
1994
+ // Since we have a promise inside a function we want to test that's not returned,
1995
+ // we will wait and stub it's last function to resolve this waiting promise.
1996
+ return new Promise((resolve) => {
1997
+ webex.meetings.destroy.callsFake(() => resolve());
1998
+ locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
1999
+ }).then(() => {
2000
+ assert.calledTwice(meeting.meetingRequest.getLocusDTO);
2001
+
2002
+ assert.deepEqual(meeting.meetingRequest.getLocusDTO.getCalls()[0].args, [{url: 'deltaSyncUrl'}]);
2003
+ assert.deepEqual(meeting.meetingRequest.getLocusDTO.getCalls()[1].args, [{url: 'fullSyncUrl'}]);
2004
+
2005
+ assert.calledWith(sendBehavioralMetricStub, 'js_sdk_locus_delta_sync_failed', {
2006
+ correlationId: meeting.correlationId,
2007
+ url: 'deltaSyncUrl',
2008
+ reason: 'fake error',
2009
+ errorName: 'Error',
2010
+ stack: sinon.match.any,
2011
+ code: sinon.match.any,
2012
+ });
2013
+
2014
+ assert.notCalled(meeting.locusInfo.handleLocusDelta);
2015
+ assert.notCalled(meeting.locusInfo.onFullLocus);
2016
+ assert.notCalled(locusInfo.locusParser.resume);
2017
+
2018
+ assert.calledOnceWithExactly(webex.meetings.destroy, meeting, 'LOCUS_DTO_SYNC_FAILED');
2019
+ });
2020
+ });
2021
+ });
2022
+
2023
+ it('onDeltaLocus handle delta data', () => {
2024
+ fakeLocus.participants = {};
2025
+ const fakeBreakout = {
2026
+ sessionId: 'sessionId',
2027
+ groupId: 'groupId',
2028
+ };
2029
+
2030
+ fakeLocus.controls = {
2031
+ breakout: fakeBreakout,
2032
+ };
2033
+ locusInfo.controls = {
2034
+ breakout: {
2035
+ sessionId: 'sessionId',
2036
+ groupId: 'groupId',
2037
+ },
2038
+ };
2039
+ locusInfo.updateParticipants = sinon.stub();
2040
+ locusInfo.onDeltaLocus(fakeLocus);
2041
+ assert.calledWith(locusInfo.updateParticipants, {}, false);
2042
+
2043
+ fakeLocus.controls.breakout.sessionId = 'sessionId2';
2044
+ locusInfo.onDeltaLocus(fakeLocus);
2045
+ assert.calledWith(locusInfo.updateParticipants, {}, true);
2046
+ });
2047
+ });
2048
+
2049
+ describe('#updateLocusCache', () => {
2050
+ it('cache it if income locus is main session locus', () => {
2051
+ const locus = {url: 'url'};
2052
+ locusInfo.mainSessionLocusCache = null;
2053
+ locusInfo.updateLocusCache(locus);
2054
+
2055
+ assert.deepEqual(locusInfo.mainSessionLocusCache, locus);
2056
+ });
2057
+
2058
+ it('not cache it if income locus is breakout session locus', () => {
2059
+ const locus = {url: 'url', controls: {breakout: {sessionType: 'BREAKOUT'}}};
2060
+ locusInfo.mainSessionLocusCache = null;
2061
+ locusInfo.updateLocusCache(locus);
2062
+
2063
+ assert.isNull(locusInfo.mainSessionLocusCache);
2064
+ });
2065
+ });
2066
+
2067
+ describe('#getTheLocusToUpdate', () => {
2068
+ it('return the cache locus if return to main session and do not clear main session cache', () => {
2069
+ locusInfo.mainSessionLocusCache = {url: 'url'};
2070
+ locusInfo.controls = {
2071
+ breakout: {
2072
+ sessionType: 'BREAKOUT',
2073
+ },
2074
+ };
2075
+ const newLocus = {
2076
+ controls: {
2077
+ breakout: {
2078
+ sessionType: 'MAIN',
2079
+ },
2080
+ },
2081
+ };
2082
+
2083
+ assert.deepEqual(locusInfo.getTheLocusToUpdate(newLocus), {url: 'url'});
2084
+
2085
+ locusInfo.clearMainSessionLocusCache = sinon.stub();
2086
+ locusInfo.getTheLocusToUpdate(newLocus);
2087
+ assert.notCalled(locusInfo.clearMainSessionLocusCache)
2088
+ });
2089
+
2090
+ it('return the new locus if return to main session but no cache and do not clear main session cache', () => {
2091
+ locusInfo.mainSessionLocusCache = null;
2092
+ locusInfo.controls = {
2093
+ breakout: {
2094
+ sessionType: 'BREAKOUT',
2095
+ },
2096
+ };
2097
+ const newLocus = {
2098
+ controls: {
2099
+ breakout: {
2100
+ sessionType: 'MAIN',
2101
+ },
2102
+ },
2103
+ };
2104
+
2105
+ assert.deepEqual(locusInfo.getTheLocusToUpdate(newLocus), newLocus);
2106
+
2107
+ locusInfo.clearMainSessionLocusCache = sinon.stub();
2108
+ locusInfo.getTheLocusToUpdate(newLocus);
2109
+ assert.notCalled(locusInfo.clearMainSessionLocusCache)
2110
+ });
2111
+
2112
+ it('return the new locus if not return to main session and clear main session cache', () => {
2113
+ locusInfo.mainSessionLocusCache = {
2114
+ controls: {
2115
+ breakout: {
2116
+ sessionType: 'MAIN',
2117
+ },
2118
+ },
2119
+ self: {removed: true}
2120
+ };
2121
+ locusInfo.fullState = {state: 'ACTIVE'}
2122
+ locusInfo.controls = {
2123
+ breakout: {
2124
+ sessionType: 'MAIN',
2125
+ },
2126
+ };
2127
+ const newLocus = {
2128
+ controls: {
2129
+ breakout: {
2130
+ sessionType: 'BREAKOUT',
2131
+ },
2132
+ },
2133
+ };
2134
+
2135
+ locusInfo.clearMainSessionLocusCache = sinon.stub();
2136
+ const result = locusInfo.getTheLocusToUpdate(newLocus);
2137
+ assert.calledOnce(locusInfo.clearMainSessionLocusCache)
2138
+
2139
+ assert.deepEqual(result, newLocus);
2140
+ });
2141
+
2142
+ it('do not clear main session cache when "mainSessionLocusCache?.self?.removed" is not true', () => {
2143
+ locusInfo.mainSessionLocusCache = {
2144
+ controls: {
2145
+ breakout: {
2146
+ sessionType: 'MAIN',
2147
+ },
2148
+ },
2149
+ self: {removed: undefined}
2150
+ };
2151
+ locusInfo.fullState = {state: 'ACTIVE'}
2152
+ locusInfo.controls = {
2153
+ breakout: {
2154
+ sessionType: 'MAIN',
2155
+ },
2156
+ };
2157
+ const newLocus = {
2158
+ controls: {
2159
+ breakout: {
2160
+ sessionType: 'BREAKOUT',
2161
+ },
2162
+ },
2163
+ };
2164
+
2165
+ locusInfo.clearMainSessionLocusCache = sinon.stub();
2166
+ locusInfo.getTheLocusToUpdate(newLocus);
2167
+ assert.notCalled(locusInfo.clearMainSessionLocusCache)
2168
+ });
2169
+ });
2170
+
2171
+ describe('#mergeParticipants', () => {
2172
+ let participants;
2173
+ let sourceParticipants;
2174
+ beforeEach(() => {
2175
+ participants = [{id: '111', status: 'JOINED'}, {id: '222'}];
2176
+ sourceParticipants = [{id: '111', status: 'LEFT'}, {id: '333'}];
2177
+ });
2178
+
2179
+ it('merge the participants, replace it by id if exist in old array', () => {
2180
+ const result = locusInfo.mergeParticipants(participants, sourceParticipants);
2181
+ assert.deepEqual(result, [{id: '111', status: 'LEFT'}, {id: '222'}, {id: '333'}]);
2182
+ });
2183
+
2184
+ it('return new participants if previous participants is empty', () => {
2185
+ const result = locusInfo.mergeParticipants([], sourceParticipants);
2186
+ assert.deepEqual(result, sourceParticipants);
2187
+ });
2188
+
2189
+ it('return new participants if previous participants is null/undefined', () => {
2190
+ let result = locusInfo.mergeParticipants(null, sourceParticipants);
2191
+ assert.deepEqual(result, sourceParticipants);
2192
+
2193
+ result = locusInfo.mergeParticipants(undefined, sourceParticipants);
2194
+ assert.deepEqual(result, sourceParticipants);
2195
+ });
2196
+
2197
+ it('return previous participants if new participants is empty', () => {
2198
+ const result = locusInfo.mergeParticipants(participants, []);
2199
+ assert.deepEqual(result, participants);
2200
+ });
2201
+
2202
+ it('return previous participants if new participants is null/undefined', () => {
2203
+ let result = locusInfo.mergeParticipants(participants, null);
2204
+ assert.deepEqual(result, participants);
2205
+
2206
+ result = locusInfo.mergeParticipants(participants, undefined);
2207
+ assert.deepEqual(result, participants);
2208
+ });
2209
+ });
2210
+
2211
+ describe('#updateMainSessionLocusCache', () => {
2212
+ let cachedLocus;
2213
+ let newLocus;
2214
+ beforeEach(() => {
2215
+ cachedLocus = {
2216
+ controls: {},
2217
+ participants: [],
2218
+ info: {webExMeetingId: 'testId1', topic: 'test'},
2219
+ };
2220
+ newLocus = {
2221
+ self: {},
2222
+ participants: [{id: '111'}],
2223
+ info: {testId: 'testId2', webExMeetingName: 'hello'},
2224
+ };
2225
+ });
2226
+ it('shallow merge new locus into cache', () => {
2227
+ locusInfo.mainSessionLocusCache = cachedLocus;
2228
+ locusInfo.updateMainSessionLocusCache(newLocus);
2229
+
2230
+ assert.deepEqual(locusInfo.mainSessionLocusCache, {
2231
+ controls: {},
2232
+ participants: [{id: '111'}],
2233
+ info: {testId: 'testId2', webExMeetingName: 'hello'},
2234
+ self: {},
2235
+ });
2236
+ });
2237
+
2238
+ it('cache new locus if no cache before', () => {
2239
+ locusInfo.mainSessionLocusCache = null;
2240
+ locusInfo.updateMainSessionLocusCache(newLocus);
2241
+
2242
+ assert.deepEqual(locusInfo.mainSessionLocusCache, newLocus);
2243
+ });
2244
+
2245
+ it('do nothing if new locus is null', () => {
2246
+ locusInfo.mainSessionLocusCache = cachedLocus;
2247
+ locusInfo.updateMainSessionLocusCache(null);
2248
+
2249
+ assert.deepEqual(locusInfo.mainSessionLocusCache, cachedLocus);
2250
+ });
2251
+ });
2252
+
2253
+ describe('#clearMainSessionLocusCache', () => {
2254
+ it('clear main session locus cache', () => {
2255
+ locusInfo.mainSessionLocusCache = {controls: {}};
2256
+ locusInfo.clearMainSessionLocusCache();
2257
+ assert.isNull(locusInfo.mainSessionLocusCache);
2258
+ });
1332
2259
  });
1333
2260
 
1334
2261
  describe('#handleOneonOneEvent', () => {
@@ -1371,5 +2298,404 @@ describe('plugin-meetings', () => {
1371
2298
  );
1372
2299
  });
1373
2300
  });
2301
+
2302
+ describe('#isMeetingActive', () => {
2303
+ it('sends client event correctly for state = inactive', () => {
2304
+ locusInfo.parsedLocus = {
2305
+ fullState: {
2306
+ type: _CALL_,
2307
+ },
2308
+ };
2309
+
2310
+ locusInfo.fullState = {
2311
+ state: LOCUS.STATE.INACTIVE,
2312
+ };
2313
+
2314
+ locusInfo.isMeetingActive();
2315
+
2316
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
2317
+ name: 'client.call.remote-ended',
2318
+ options: {
2319
+ meetingId: locusInfo.meetingId,
2320
+ },
2321
+ });
2322
+ });
2323
+
2324
+ it('sends client event correctly for state = PARTNER_LEFT', () => {
2325
+ locusInfo.getLocusPartner = sinon.stub().returns({state: MEETING_STATE.STATES.LEFT});
2326
+ locusInfo.parsedLocus = {
2327
+ fullState: {
2328
+ type: _CALL_,
2329
+ },
2330
+ self: {
2331
+ state: MEETING_STATE.STATES.DECLINED,
2332
+ },
2333
+ };
2334
+ locusInfo.isMeetingActive();
2335
+
2336
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
2337
+ name: 'client.call.remote-ended',
2338
+ options: {
2339
+ meetingId: locusInfo.meetingId,
2340
+ },
2341
+ });
2342
+ });
2343
+
2344
+ it('sends client event correctly for state = SELF_LEFT', () => {
2345
+ locusInfo.getLocusPartner = sinon.stub().returns({state: MEETING_STATE.STATES.LEFT});
2346
+ locusInfo.parsedLocus = {
2347
+ fullState: {
2348
+ type: _CALL_,
2349
+ },
2350
+ self: {
2351
+ state: MEETING_STATE.STATES.LEFT,
2352
+ },
2353
+ };
2354
+
2355
+ locusInfo.isMeetingActive();
2356
+
2357
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
2358
+ name: 'client.call.remote-ended',
2359
+ options: {
2360
+ meetingId: locusInfo.meetingId,
2361
+ },
2362
+ });
2363
+ });
2364
+
2365
+ it('sends client event correctly for state = MEETING_INACTIVE_TERMINATING', () => {
2366
+ locusInfo.getLocusPartner = sinon.stub().returns({state: MEETING_STATE.STATES.LEFT});
2367
+ locusInfo.parsedLocus = {
2368
+ fullState: {
2369
+ type: _MEETING_,
2370
+ },
2371
+ };
2372
+
2373
+ locusInfo.fullState = {
2374
+ state: LOCUS.STATE.INACTIVE,
2375
+ };
2376
+
2377
+ locusInfo.isMeetingActive();
2378
+
2379
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
2380
+ name: 'client.call.remote-ended',
2381
+ options: {
2382
+ meetingId: locusInfo.meetingId,
2383
+ },
2384
+ });
2385
+ });
2386
+
2387
+ it('sends client event correctly for state = FULLSTATE_REMOVED', () => {
2388
+ locusInfo.getLocusPartner = sinon.stub().returns({state: MEETING_STATE.STATES.LEFT});
2389
+ locusInfo.parsedLocus = {
2390
+ fullState: {
2391
+ type: _MEETING_,
2392
+ },
2393
+ };
2394
+
2395
+ locusInfo.fullState = {
2396
+ removed: true,
2397
+ };
2398
+
2399
+ locusInfo.isMeetingActive();
2400
+
2401
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
2402
+ name: 'client.call.remote-ended',
2403
+ options: {
2404
+ meetingId: locusInfo.meetingId,
2405
+ },
2406
+ });
2407
+ });
2408
+ });
2409
+
2410
+ // semi-integration tests that use real LocusInfo with real Parser
2411
+ // and test various scenarios related to handling out-of-order Locus delta events
2412
+ describe('handling of out-of-order Locus delta events', () => {
2413
+ let clock;
2414
+
2415
+ const generateDeltaEvent = (base, sequence) => {
2416
+ return {
2417
+ baseSequence: {
2418
+ rangeStart: 0,
2419
+ rangeEnd: 0,
2420
+ entries: [base],
2421
+ },
2422
+ sequence: {
2423
+ rangeStart: 0,
2424
+ rangeEnd: 0,
2425
+ entries: [sequence],
2426
+ },
2427
+ syncUrl: `fake sync url for sequence ${sequence}`,
2428
+ self: {
2429
+ person: {
2430
+ id: 'test person id',
2431
+ },
2432
+ },
2433
+ };
2434
+ };
2435
+
2436
+ // a list of example delta events, sorted by time and each event is based on the previous one
2437
+ const deltaEvents = [
2438
+ generateDeltaEvent(10, 20), // 0
2439
+ generateDeltaEvent(20, 30), // 1
2440
+ generateDeltaEvent(30, 40), // 2
2441
+ generateDeltaEvent(40, 50), // 3
2442
+ generateDeltaEvent(50, 60), // 4
2443
+ generateDeltaEvent(60, 70), // 5
2444
+ generateDeltaEvent(70, 80), // 6
2445
+ generateDeltaEvent(80, 90), // 7
2446
+ generateDeltaEvent(90, 100), // 8
2447
+ ];
2448
+
2449
+ let updateLocusInfoStub; // we use this stub to verify that an event has been fully processed
2450
+ let syncRequestStub;
2451
+
2452
+ beforeEach(() => {
2453
+ clock = sinon.useFakeTimers();
2454
+
2455
+ sinon.stub(locusInfo, 'updateParticipantDeltas');
2456
+ sinon.stub(locusInfo, 'updateParticipants');
2457
+ sinon.stub(locusInfo, 'isMeetingActive'),
2458
+ sinon.stub(locusInfo, 'handleOneOnOneEvent'),
2459
+ (updateLocusInfoStub = sinon.stub(locusInfo, 'updateLocusInfo'));
2460
+ syncRequestStub = sinon.stub().resolves({body: {}});
2461
+
2462
+ mockMeeting.locusInfo = locusInfo;
2463
+ mockMeeting.locusUrl = 'fake locus url';
2464
+ mockMeeting.meetingRequest = {
2465
+ getLocusDTO: syncRequestStub,
2466
+ };
2467
+
2468
+ locusInfo.onFullLocus({
2469
+ sequence: {
2470
+ rangeStart: 0,
2471
+ rangeEnd: 0,
2472
+ entries: [10],
2473
+ },
2474
+ self: {
2475
+ person: {
2476
+ id: 'test person id',
2477
+ },
2478
+ },
2479
+ });
2480
+
2481
+ updateLocusInfoStub.resetHistory();
2482
+ });
2483
+
2484
+ afterEach(() => {
2485
+ clock.restore();
2486
+ });
2487
+
2488
+ it('queues out-of-order deltas until it receives a correct delta', () => {
2489
+ // send some out-of-order deltas
2490
+ locusInfo.handleLocusDelta(deltaEvents[1], mockMeeting);
2491
+ locusInfo.handleLocusDelta(deltaEvents[4], mockMeeting);
2492
+
2493
+ // they should be queued and not processed
2494
+ assert.notCalled(updateLocusInfoStub);
2495
+
2496
+ // now one of the missing ones, but not the one SDK is really waiting for
2497
+ locusInfo.handleLocusDelta(deltaEvents[2], mockMeeting);
2498
+
2499
+ // still nothing should be processed
2500
+ assert.notCalled(updateLocusInfoStub);
2501
+
2502
+ // now send the one SDK is waiting for
2503
+ locusInfo.handleLocusDelta(deltaEvents[0], mockMeeting);
2504
+
2505
+ // so deltaEvents with indexes 1,2,3 can be processed, but 5 still not, because 4 is missing
2506
+ assert.callCount(updateLocusInfoStub, 3);
2507
+ assert.calledWith(updateLocusInfoStub.getCall(0), deltaEvents[0]);
2508
+ assert.calledWith(updateLocusInfoStub.getCall(1), deltaEvents[1]);
2509
+ assert.calledWith(updateLocusInfoStub.getCall(2), deltaEvents[2]);
2510
+
2511
+ updateLocusInfoStub.resetHistory();
2512
+
2513
+ // now send deltaEvents[4]
2514
+ locusInfo.handleLocusDelta(deltaEvents[3], mockMeeting);
2515
+
2516
+ // and verify deltaEvents[4] and deltaEvents[5] have been processed
2517
+ assert.callCount(updateLocusInfoStub, 2);
2518
+ assert.calledWith(updateLocusInfoStub.getCall(0), deltaEvents[3]);
2519
+ assert.calledWith(updateLocusInfoStub.getCall(1), deltaEvents[4]);
2520
+ });
2521
+
2522
+ it('handles out-of-order deltas correctly even if all arrive in reverse order', () => {
2523
+ // send a bunch deltas in reverse order
2524
+ for (let i = 4; i >= 0; i--) {
2525
+ locusInfo.handleLocusDelta(deltaEvents[i], mockMeeting);
2526
+ }
2527
+
2528
+ // they should be queued and then processed in correct order
2529
+ assert.callCount(updateLocusInfoStub, 5);
2530
+ assert.calledWith(updateLocusInfoStub.getCall(0), deltaEvents[0]);
2531
+ assert.calledWith(updateLocusInfoStub.getCall(1), deltaEvents[1]);
2532
+ assert.calledWith(updateLocusInfoStub.getCall(2), deltaEvents[2]);
2533
+ assert.calledWith(updateLocusInfoStub.getCall(3), deltaEvents[3]);
2534
+ assert.calledWith(updateLocusInfoStub.getCall(4), deltaEvents[4]);
2535
+ });
2536
+
2537
+ it('sends a sync request using syncUrl if it receives at least 1 delta event and processes later deltas after sync correctly', async () => {
2538
+ // the test first sends an initial "good" delta
2539
+ const initialDeltaIdx = 0;
2540
+ const initialDelta = deltaEvents[initialDeltaIdx];
2541
+
2542
+ // then it sends a bunch of out-of-order deltas (at least 6 to trigger a sync), last one being lastOooDelta
2543
+ const firstOooDeltaIdx = 2;
2544
+ const lastOooDeltaIdx = 7;
2545
+ const lastOooDelta = deltaEvents[lastOooDeltaIdx];
2546
+
2547
+ // and finally, after the sync it sends another "good" delta
2548
+ const goodDeltaAfterSync = deltaEvents[8];
2549
+
2550
+ const deltaLocusFromSyncResponse = {
2551
+ baseSequence: {
2552
+ rangeStart: 0,
2553
+ rangeEnd: 0,
2554
+ entries: [initialDelta.sequence.entries[0]],
2555
+ },
2556
+ sequence: {
2557
+ rangeStart: 0,
2558
+ rangeEnd: 0,
2559
+ entries: [lastOooDelta.sequence.entries[0]],
2560
+ },
2561
+ syncUrl: `fake sync url for sequence ${lastOooDelta.sequence.entries[0]}`,
2562
+ self: {
2563
+ person: {
2564
+ id: 'test person id',
2565
+ },
2566
+ },
2567
+ };
2568
+
2569
+ syncRequestStub.resolves({
2570
+ body: deltaLocusFromSyncResponse,
2571
+ });
2572
+
2573
+ // send one correct delta so that SDK has the syncUrl
2574
+ locusInfo.handleLocusDelta(initialDelta, mockMeeting);
2575
+
2576
+ updateLocusInfoStub.resetHistory();
2577
+
2578
+ // send 6 out-of-order deltas to trigger a sync (we're skipping deltaEvents[1])
2579
+ for (let i = firstOooDeltaIdx; i <= lastOooDeltaIdx; i++) {
2580
+ locusInfo.handleLocusDelta(deltaEvents[i], mockMeeting);
2581
+ }
2582
+
2583
+ await testUtils.flushPromises();
2584
+
2585
+ // check that sync was done using the correct syncUrl
2586
+ assert.calledOnceWithExactly(syncRequestStub, {url: initialDelta.syncUrl});
2587
+ assert.calledOnceWithExactly(updateLocusInfoStub, deltaLocusFromSyncResponse);
2588
+
2589
+ updateLocusInfoStub.resetHistory();
2590
+
2591
+ // now send another delta - a good one, it should be processed as normal
2592
+ locusInfo.handleLocusDelta(goodDeltaAfterSync, mockMeeting);
2593
+
2594
+ assert.calledOnceWithExactly(updateLocusInfoStub, goodDeltaAfterSync);
2595
+ });
2596
+
2597
+ it('does a sync if blocked on out-of-order deltas for too long', async () => {
2598
+ // stub random so that the timer fires after 12500 ms
2599
+ sinon.stub(Math, 'random').returns(0.5);
2600
+
2601
+ const oooDelta = deltaEvents[3];
2602
+
2603
+ // setup the stubs so that the sync request receives a full DTO with the sequence equal to the out-of-order delta we simulate
2604
+ const fullLocus = {
2605
+ sequence: oooDelta.sequence,
2606
+ };
2607
+ syncRequestStub.resolves({
2608
+ body: fullLocus,
2609
+ });
2610
+
2611
+ // send an out-of-order delta
2612
+ locusInfo.handleLocusDelta(oooDelta, mockMeeting);
2613
+
2614
+ assert.calledOnceWithExactly(sendBehavioralMetricStub, 'js_sdk_locus_delta_ooo', { stack: sinon.match.any})
2615
+
2616
+ await clock.tickAsync(12499);
2617
+ await testUtils.flushPromises();
2618
+ assert.notCalled(syncRequestStub);
2619
+ assert.notCalled(updateLocusInfoStub);
2620
+
2621
+ await clock.tickAsync(1);
2622
+ await testUtils.flushPromises();
2623
+
2624
+ assert.calledOnceWithExactly(syncRequestStub, {url: mockMeeting.locusUrl});
2625
+ assert.calledOnceWithExactly(updateLocusInfoStub, fullLocus);
2626
+ });
2627
+
2628
+ it('does a sync if out-of-order deltas queue becomes too big', async () => {
2629
+ // setup the stubs so that the sync request receives a full DTO with the sequence equal to the out-of-order delta we simulate
2630
+ const fullLocus = {
2631
+ sequence: deltaEvents[6].sequence,
2632
+ };
2633
+ syncRequestStub.resolves({
2634
+ body: fullLocus,
2635
+ });
2636
+
2637
+ // send 5 deltas, starting from deltaEvents[1] so that SDK is blocked waiting for deltaEvents[0]
2638
+ for (let i = 0; i < 5; i++) {
2639
+ locusInfo.handleLocusDelta(deltaEvents[i + 1], mockMeeting);
2640
+ }
2641
+
2642
+ // nothing should happen, SDK should still be waiting for deltaEvents[0]
2643
+ assert.notCalled(syncRequestStub);
2644
+ assert.notCalled(updateLocusInfoStub);
2645
+
2646
+ // now send one more out-of-order delta to trigger a sync request
2647
+ locusInfo.handleLocusDelta(deltaEvents[6], mockMeeting);
2648
+
2649
+ await testUtils.flushPromises();
2650
+
2651
+ // check sync was done
2652
+ assert.calledOnceWithExactly(syncRequestStub, {url: mockMeeting.locusUrl});
2653
+ assert.calledOnceWithExactly(updateLocusInfoStub, fullLocus);
2654
+ });
2655
+
2656
+ it('processes delta events that are not included in sync response', async () => {
2657
+ // this test sends a bunch of out-of-order deltas, this triggers a sync
2658
+ // but the full locus response doesn't include the last 2 deltas received, so
2659
+ // we check that these 2 deltas are also processed after sync response
2660
+ const fullLocusFromSyncResponse = {
2661
+ baseSequence: {
2662
+ rangeStart: 0,
2663
+ rangeEnd: 0,
2664
+ entries: [deltaEvents[0].sequence.entries[0]],
2665
+ },
2666
+ sequence: {
2667
+ rangeStart: 0,
2668
+ rangeEnd: 0,
2669
+ entries: [deltaEvents[5].sequence.entries[0]],
2670
+ },
2671
+ syncUrl: `fake sync url for sequence ${deltaEvents[5].sequence.entries[0]}`,
2672
+ self: {
2673
+ person: {
2674
+ id: 'test person id',
2675
+ },
2676
+ },
2677
+ };
2678
+
2679
+ syncRequestStub.resolves({
2680
+ body: fullLocusFromSyncResponse,
2681
+ });
2682
+
2683
+ // send at least 6 out-of-order deltas to trigger a sync (we're skipping deltaEvents[0])
2684
+ for (let i = 1; i <= 7; i++) {
2685
+ locusInfo.handleLocusDelta(deltaEvents[i], mockMeeting);
2686
+ }
2687
+
2688
+ await testUtils.flushPromises();
2689
+
2690
+ // check that sync was done
2691
+ assert.calledOnceWithExactly(syncRequestStub, {url: mockMeeting.locusUrl});
2692
+
2693
+ // and that remaining deltas from the queue that were not included in full Locus were also processed
2694
+ assert.callCount(updateLocusInfoStub, 3);
2695
+ assert.calledWith(updateLocusInfoStub.getCall(0), fullLocusFromSyncResponse);
2696
+ assert.calledWith(updateLocusInfoStub.getCall(1), deltaEvents[6]);
2697
+ assert.calledWith(updateLocusInfoStub.getCall(2), deltaEvents[7]);
2698
+ });
2699
+ });
1374
2700
  });
1375
2701
  });