2
0

roster.js 68 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357
  1. (function (root, factory) {
  2. define(["jquery", "jasmine", "mock", "test-utils"], factory);
  3. } (this, function ($, jasmine, mock, test_utils) {
  4. const $iq = converse.env.$iq;
  5. const $msg = converse.env.$msg;
  6. const $pres = converse.env.$pres;
  7. const Strophe = converse.env.Strophe;
  8. const _ = converse.env._;
  9. const sizzle = converse.env.sizzle;
  10. const u = converse.env.utils;
  11. const checkHeaderToggling = async function (group) {
  12. var toggle = group.querySelector('a.group-toggle');
  13. expect(u.isVisible(group)).toBeTruthy();
  14. expect(group.querySelectorAll('ul.collapsed').length).toBe(0);
  15. expect(u.hasClass('fa-caret-right', toggle.firstElementChild)).toBeFalsy();
  16. expect(u.hasClass('fa-caret-down', toggle.firstElementChild)).toBeTruthy();
  17. toggle.click();
  18. await test_utils.waitUntil(() => group.querySelectorAll('ul.collapsed').length === 1);
  19. expect(u.hasClass('fa-caret-right', toggle.firstElementChild)).toBeTruthy();
  20. expect(u.hasClass('fa-caret-down', toggle.firstElementChild)).toBeFalsy();
  21. toggle.click();
  22. await test_utils.waitUntil(() => group.querySelectorAll('li').length === $(group).find('li:visible').length);
  23. expect(u.hasClass('fa-caret-right', toggle.firstElementChild)).toBeFalsy();
  24. expect(u.hasClass('fa-caret-down', toggle.firstElementChild)).toBeTruthy();
  25. };
  26. describe("The Contacts Roster", function () {
  27. it("is populated once we have registered a presence handler",
  28. mock.initConverse(
  29. null, ['rosterGroupsFetched'], {},
  30. async function (done, _converse) {
  31. spyOn(_converse.api, "trigger").and.callThrough();
  32. const IQs = _converse.connection.IQ_stanzas;
  33. const node = await test_utils.waitUntil(
  34. () => _.filter(IQs, iq => iq.nodeTree.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop());
  35. expect(_converse.api.trigger.calls.all().map(c => c.args[0]).includes('rosterContactsFetched')).toBeFalsy();
  36. expect(node.toLocaleString()).toBe(
  37. `<iq id="${node.nodeTree.getAttribute('id')}" type="get" xmlns="jabber:client">`+
  38. `<query xmlns="jabber:iq:roster"/>`+
  39. `</iq>`);
  40. const result = $iq({
  41. 'to': _converse.connection.jid,
  42. 'type': 'result',
  43. 'id': node.nodeTree.getAttribute('id')
  44. }).c('query', {
  45. 'xmlns': 'jabber:iq:roster'
  46. }).c('item', {'jid': 'nurse@example.com'}).up()
  47. .c('item', {'jid': 'romeo@example.com'})
  48. _converse.connection._dataRecv(test_utils.createRequest(result));
  49. await test_utils.waitUntil(() => _converse.api.trigger.calls.all().map(c => c.args[0]).includes('rosterContactsFetched'));
  50. done();
  51. }));
  52. it("supports roster versioning",
  53. mock.initConverse(
  54. null, ['rosterGroupsFetched'], {},
  55. async function (done, _converse) {
  56. const IQ_stanzas = _converse.connection.IQ_stanzas;
  57. let node = await test_utils.waitUntil(
  58. () => _.filter(IQ_stanzas, iq => iq.nodeTree.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop()
  59. );
  60. let stanza = node.nodeTree;
  61. expect(_converse.roster.data.get('version')).toBeUndefined();
  62. expect(node.toLocaleString()).toBe(
  63. `<iq id="${stanza.getAttribute('id')}" type="get" xmlns="jabber:client">`+
  64. `<query xmlns="jabber:iq:roster"/>`+
  65. `</iq>`);
  66. let result = $iq({
  67. 'to': _converse.connection.jid,
  68. 'type': 'result',
  69. 'id': stanza.getAttribute('id')
  70. }).c('query', {
  71. 'xmlns': 'jabber:iq:roster',
  72. 'ver': 'ver7'
  73. }).c('item', {'jid': 'nurse@example.com'}).up()
  74. .c('item', {'jid': 'romeo@example.com'})
  75. _converse.connection._dataRecv(test_utils.createRequest(result));
  76. await test_utils.waitUntil(() => _converse.roster.models.length > 1);
  77. expect(_converse.roster.data.get('version')).toBe('ver7');
  78. expect(_converse.roster.models.length).toBe(2);
  79. _converse.roster.fetchFromServer();
  80. node = _converse.connection.IQ_stanzas.pop();
  81. stanza = node.nodeTree;
  82. expect(node.toLocaleString()).toBe(
  83. `<iq id="${stanza.getAttribute('id')}" type="get" xmlns="jabber:client">`+
  84. `<query ver="ver7" xmlns="jabber:iq:roster"/>`+
  85. `</iq>`);
  86. result = $iq({
  87. 'to': _converse.connection.jid,
  88. 'type': 'result',
  89. 'id': stanza.getAttribute('id')
  90. });
  91. _converse.connection._dataRecv(test_utils.createRequest(result));
  92. const roster_push = $iq({
  93. 'to': _converse.connection.jid,
  94. 'type': 'set',
  95. }).c('query', {'xmlns': 'jabber:iq:roster', 'ver': 'ver34'})
  96. .c('item', {'jid': 'romeo@example.com', 'subscription': 'remove'});
  97. _converse.connection._dataRecv(test_utils.createRequest(roster_push));
  98. expect(_converse.roster.data.get('version')).toBe('ver34');
  99. expect(_converse.roster.models.length).toBe(1);
  100. expect(_converse.roster.at(0).get('jid')).toBe('nurse@example.com');
  101. done();
  102. }));
  103. describe("The live filter", function () {
  104. it("will only appear when roster contacts flow over the visible area",
  105. mock.initConverse(
  106. null, ['rosterGroupsFetched'], {},
  107. function (done, _converse) {
  108. var $filter = $(_converse.rosterview.el.querySelector('.roster-filter'));
  109. var names = mock.cur_names;
  110. test_utils.openControlBox();
  111. _converse.rosterview.update(); // XXX: Will normally called as event handler
  112. expect($filter.length).toBe(1);
  113. test_utils.waitUntil(function () {
  114. return !$filter.is(':visible');
  115. }).then(function () {
  116. for (var i=0; i<names.length; i++) {
  117. _converse.roster.create({
  118. ask: null,
  119. fullname: names[i],
  120. jid: names[i].replace(/ /g,'.').toLowerCase() + '@localhost',
  121. requesting: 'false',
  122. subscription: 'both'
  123. });
  124. _converse.rosterview.update(); // XXX: Will normally called as event handler
  125. }
  126. $.fn.hasScrollBar = function() {
  127. if (!$.contains(document, this.get(0))) {
  128. return false;
  129. }
  130. if(this.parent().height() < this.get(0).scrollHeight) {
  131. return true;
  132. }
  133. return false;
  134. };
  135. return test_utils.waitUntil(function () {
  136. if ($(_converse.rosterview.roster_el).hasScrollBar()) {
  137. return $filter.is(':visible');
  138. } else {
  139. return !$filter.is(':visible');
  140. }
  141. }).then(function () {
  142. done();
  143. });
  144. });
  145. }));
  146. it("can be used to filter the contacts shown",
  147. mock.initConverse(
  148. null, ['rosterGroupsFetched'], {},
  149. function (done, _converse) {
  150. _converse.roster_groups = true;
  151. test_utils.openControlBox();
  152. test_utils.createGroupedContacts(_converse);
  153. var $filter = $(_converse.rosterview.el).find('.roster-filter');
  154. var $roster = $(_converse.rosterview.roster_el);
  155. _converse.rosterview.filter_view.delegateEvents();
  156. var promise = test_utils.waitUntil(() => $roster.find('li:visible').length === 15, 600)
  157. .then(function (contacts) {
  158. expect($roster.find('ul.roster-group-contacts:visible').length).toBe(5);
  159. $filter[0].value = "candice";
  160. u.triggerEvent($filter[0], "keydown", "KeyboardEvent");
  161. return test_utils.waitUntil(() => $roster.find('li:visible').length === 1, 600);
  162. }).then(function (contacts) {
  163. // Only one roster contact is now visible
  164. expect($roster.find('li:visible').length).toBe(1);
  165. expect($roster.find('li:visible').eq(0).text().trim()).toBe('Candice van der Knijff');
  166. // Only one foster group is still visible
  167. expect($roster.find('.roster-group:visible').length).toBe(1);
  168. expect(_.trim($roster.find('.roster-group:visible a.group-toggle').eq(0).text())).toBe('colleagues');
  169. $filter = $(_converse.rosterview.el).find('.roster-filter');
  170. $filter.val("an");
  171. u.triggerEvent($filter[0], "keydown", "KeyboardEvent");
  172. return test_utils.waitUntil(function () {
  173. return $roster.find('li:visible').length === 5;
  174. }, 600)
  175. }).then(function (contacts) {
  176. // Five roster contact is now visible
  177. expect($roster.find('li:visible').length).toBe(5);
  178. // Four groups are still visible
  179. var $groups = $roster.find('.roster-group:visible a.group-toggle');
  180. expect($groups.length).toBe(4);
  181. expect(_.trim($groups.eq(0).text())).toBe('colleagues');
  182. expect(_.trim($groups.eq(1).text())).toBe('Family');
  183. expect(_.trim($groups.eq(2).text())).toBe('friends & acquaintences');
  184. expect(_.trim($groups.eq(3).text())).toBe('ænemies');
  185. $filter = $(_converse.rosterview.el).find('.roster-filter');
  186. $filter.val("xxx");
  187. u.triggerEvent($filter[0], "keydown", "KeyboardEvent");
  188. return test_utils.waitUntil(function () {
  189. return $roster.find('li:visible').length === 0;
  190. }, 600)
  191. }).then(function () {
  192. expect($roster.find('ul.roster-group-contacts:visible a.group-toggle').length).toBe(0);
  193. $filter = $(_converse.rosterview.el).find('.roster-filter');
  194. $filter.val(""); // Check that contacts are shown again, when the filter string is cleared.
  195. u.triggerEvent($filter[0], "keydown", "KeyboardEvent");
  196. return test_utils.waitUntil(function () {
  197. return $roster.find('li:visible').length === 15;
  198. }, 600)
  199. }).then(function () {
  200. expect($roster.find('ul.roster-group-contacts:visible').length).toBe(5);
  201. _converse.roster_groups = false;
  202. done();
  203. }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
  204. }));
  205. it("will also filter out contacts added afterwards",
  206. mock.initConverse(
  207. null, ['rosterGroupsFetched'], {},
  208. function (done, _converse) {
  209. test_utils.openControlBox();
  210. test_utils.createGroupedContacts(_converse);
  211. var $filter = $(_converse.rosterview.el).find('.roster-filter');
  212. var $roster = $(_converse.rosterview.roster_el);
  213. _converse.rosterview.filter_view.delegateEvents();
  214. test_utils.waitUntil(function () {
  215. return $roster.find('li:visible').length === 15;
  216. }, 300).then(function (contacts) {
  217. $filter.val("an");
  218. u.triggerEvent($filter[0], "keydown", "KeyboardEvent");
  219. return test_utils.waitUntil(function () {
  220. return $roster.find('li:visible').length === 5;
  221. }, 500)
  222. }).then(function (contacts) {
  223. // Five roster contact is now visible
  224. expect($roster.find('li:visible').length).toBe(5);
  225. // Four groups are still visible
  226. var $groups = $roster.find('.roster-group:visible a.group-toggle');
  227. expect($groups.length).toBe(4);
  228. expect(_.trim($groups.eq(0).text())).toBe('colleagues');
  229. expect(_.trim($groups.eq(1).text())).toBe('Family');
  230. expect(_.trim($groups.eq(2).text())).toBe('friends & acquaintences');
  231. expect(_.trim($groups.eq(3).text())).toBe('ænemies');
  232. _converse.roster.create({
  233. jid: 'latecomer@localhost',
  234. subscription: 'both',
  235. ask: null,
  236. groups: ['newgroup'],
  237. fullname: 'Marty McLatecomer'
  238. });
  239. return test_utils.waitUntil(function () {
  240. return $roster.find('.roster-group[data-group="newgroup"] li').length;
  241. }, 300);
  242. }).then(function (contacts) {
  243. // The "newgroup" group doesn't appear
  244. expect($roster.find('.roster-group:visible').length).toBe(4);
  245. expect($roster.find('.roster-group').length).toBe(6);
  246. done();
  247. });
  248. }));
  249. it("can be used to filter the groups shown",
  250. mock.initConverse(
  251. null, ['rosterGroupsFetched'], {},
  252. function (done, _converse) {
  253. _converse.roster_groups = true;
  254. test_utils.openControlBox();
  255. test_utils.createGroupedContacts(_converse);
  256. _converse.rosterview.filter_view.delegateEvents();
  257. var $roster = $(_converse.rosterview.roster_el);
  258. var button = _converse.rosterview.el.querySelector('span[data-type="groups"]');
  259. button.click();
  260. test_utils.waitUntil(function () {
  261. return $roster.find('li:visible').length === 15;
  262. }, 600).then(function () {
  263. expect($roster.find('div.roster-group:visible a.group-toggle').length).toBe(5);
  264. var filter = _converse.rosterview.el.querySelector('.roster-filter');
  265. filter.value = "colleagues";
  266. u.triggerEvent(filter, "keydown", "KeyboardEvent");
  267. return test_utils.waitUntil(function () {
  268. return $roster.find('div.roster-group:not(.collapsed) a.group-toggle').length === 1;
  269. }, 600);
  270. }).then(function () {
  271. expect(_.trim($roster.find('div.roster-group:not(.collapsed) a').eq(0).text())).toBe('colleagues');
  272. expect($roster.find('div.roster-group:not(.collapsed) li:visible').length).toBe(3);
  273. // Check that all contacts under the group are shown
  274. expect($roster.find('div.roster-group:not(.collapsed) li:hidden').length).toBe(0);
  275. var filter = _converse.rosterview.el.querySelector('.roster-filter');
  276. filter.value = "xxx";
  277. u.triggerEvent(filter, "keydown", "KeyboardEvent");
  278. return test_utils.waitUntil(function () {
  279. return $roster.find('div.roster-group.collapsed a.group-toggle').length === 5;
  280. }, 700);
  281. }).then(function () {
  282. expect($roster.find('div.roster-group:not(.collapsed) a').length).toBe(0);
  283. var filter = _converse.rosterview.el.querySelector('.roster-filter');
  284. filter.value = ""; // Check that groups are shown again, when the filter string is cleared.
  285. u.triggerEvent(filter, "keydown", "KeyboardEvent");
  286. return test_utils.waitUntil(function () {
  287. return $roster.find('div.roster-group.collapsed a.group-toggle').length === 0;
  288. }, 600);
  289. }).then(function () {
  290. expect($roster.find('div.roster-group:not(collapsed)').length).toBe(5);
  291. expect($roster.find('div.roster-group:not(collapsed) li').length).toBe(15);
  292. done();
  293. });
  294. }));
  295. it("has a button with which its contents can be cleared",
  296. mock.initConverse(
  297. null, ['rosterGroupsFetched'], {},
  298. function (done, _converse) {
  299. _converse.roster_groups = true;
  300. test_utils.openControlBox();
  301. test_utils.createGroupedContacts(_converse);
  302. var filter = _converse.rosterview.el.querySelector('.roster-filter');
  303. filter.value = "xxx";
  304. u.triggerEvent(filter, "keydown", "KeyboardEvent");
  305. expect(_.includes(filter.classList, "x")).toBeFalsy();
  306. expect(u.hasClass('hidden', _converse.rosterview.el.querySelector('.roster-filter-form .clear-input'))).toBeTruthy();
  307. test_utils.waitUntil(function () {
  308. return !u.hasClass('hidden', _converse.rosterview.el.querySelector('.roster-filter-form .clear-input'));
  309. }, 900).then(function () {
  310. var filter = _converse.rosterview.el.querySelector('.roster-filter');
  311. _converse.rosterview.el.querySelector('.clear-input').click();
  312. expect(document.querySelector('.roster-filter').value).toBe("");
  313. done();
  314. });
  315. }));
  316. it("can be used to filter contacts by their chat state",
  317. mock.initConverse(
  318. null, ['rosterGroupsFetched'], {},
  319. function (done, _converse) {
  320. test_utils.createGroupedContacts(_converse);
  321. var jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@localhost';
  322. _converse.roster.get(jid).presence.set('show', 'online');
  323. jid = mock.cur_names[4].replace(/ /g,'.').toLowerCase() + '@localhost';
  324. _converse.roster.get(jid).presence.set('show', 'dnd');
  325. test_utils.openControlBox();
  326. var button = _converse.rosterview.el.querySelector('span[data-type="state"]');
  327. button.click();
  328. var $roster = $(_converse.rosterview.roster_el);
  329. test_utils.waitUntil(() => $roster.find('li:visible').length === 15, 500).then(function () {
  330. var filter = _converse.rosterview.el.querySelector('.state-type');
  331. expect($roster.find('ul.roster-group-contacts:visible').length).toBe(5);
  332. filter.value = "online";
  333. u.triggerEvent(filter, 'change');
  334. return test_utils.waitUntil(() => $roster.find('li:visible').length === 1, 500);
  335. }).then(function () {
  336. expect($roster.find('li:visible').eq(0).text().trim()).toBe('Rinse Sommer');
  337. expect($roster.find('ul.roster-group-contacts:visible').length).toBe(1);
  338. var filter = _converse.rosterview.el.querySelector('.state-type');
  339. filter.value = "dnd";
  340. u.triggerEvent(filter, 'change');
  341. return test_utils.waitUntil(function () {
  342. return $roster.find('li:visible').eq(0).text().trim() === 'Annegreet Gomez';
  343. }, 900)
  344. }).then(function () {
  345. expect($roster.find('ul.roster-group-contacts:visible').length).toBe(1);
  346. done();
  347. }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
  348. }));
  349. });
  350. describe("A Roster Group", function () {
  351. it("can be used to organize existing contacts",
  352. mock.initConverse(
  353. null, ['rosterGroupsFetched'], {},
  354. async function (done, _converse) {
  355. _converse.roster_groups = true;
  356. spyOn(_converse.rosterview, 'update').and.callThrough();
  357. _converse.rosterview.render();
  358. test_utils.openControlBox();
  359. test_utils.createContacts(_converse, 'pending');
  360. test_utils.createContacts(_converse, 'requesting');
  361. test_utils.createGroupedContacts(_converse);
  362. // Check that the groups appear alphabetically and that
  363. // requesting and pending contacts are last.
  364. await test_utils.waitUntil(() => sizzle('.roster-group a.group-toggle', _converse.rosterview.el).length);
  365. const group_titles = _.map(
  366. sizzle('.roster-group a.group-toggle', _converse.rosterview.el),
  367. o => o.textContent.trim()
  368. );
  369. expect(group_titles).toEqual([
  370. "Contact requests",
  371. "colleagues",
  372. "Family",
  373. "friends & acquaintences",
  374. "ænemies",
  375. "Ungrouped",
  376. "Pending contacts"
  377. ]);
  378. // Check that usernames appear alphabetically per group
  379. let names;
  380. _.each(_.keys(mock.groups), function (name) {
  381. const contacts = sizzle('.roster-group[data-group="'+name+'"] ul', _converse.rosterview.el);
  382. const names = _.map(contacts, o => o.textContent.trim());
  383. expect(names).toEqual(_.clone(names).sort());
  384. });
  385. done();
  386. }));
  387. it("gets created when a contact's \"groups\" attribute changes",
  388. mock.initConverse(
  389. null, ['rosterGroupsFetched'], {},
  390. async function (done, _converse) {
  391. _converse.roster_groups = true;
  392. spyOn(_converse.rosterview, 'update').and.callThrough();
  393. _converse.rosterview.render();
  394. test_utils.openControlBox();
  395. _converse.roster.create({
  396. jid: 'groupchanger@localhost',
  397. subscription: 'both',
  398. ask: null,
  399. groups: ['firstgroup'],
  400. fullname: 'George Groupchanger'
  401. });
  402. // Check that the groups appear alphabetically and that
  403. // requesting and pending contacts are last.
  404. let group_titles = await test_utils.waitUntil(() => {
  405. const toggles = sizzle('.roster-group a.group-toggle', _converse.rosterview.el);
  406. if (_.reduce(toggles, (result, t) => result && u.isVisible(t), true)) {
  407. return _.map(toggles, o => o.textContent.trim());
  408. } else {
  409. return false;
  410. }
  411. }, 1000);
  412. expect(group_titles).toEqual(['firstgroup']);
  413. const contact = _converse.roster.get('groupchanger@localhost');
  414. contact.set({'groups': ['secondgroup']});
  415. group_titles = await test_utils.waitUntil(() => {
  416. const toggles = sizzle('.roster-group[data-group="secondgroup"] a.group-toggle', _converse.rosterview.el);
  417. if (_.reduce(toggles, (result, t) => result && u.isVisible(t), true)) {
  418. return _.map(toggles, o => o.textContent.trim());
  419. } else {
  420. return false;
  421. }
  422. }, 1000);
  423. expect(group_titles).toEqual(['secondgroup']);
  424. done();
  425. }));
  426. it("can share contacts with other roster groups",
  427. mock.initConverse(
  428. null, ['rosterGroupsFetched'], {},
  429. function (done, _converse) {
  430. _converse.roster_groups = true;
  431. var groups = ['colleagues', 'friends'];
  432. spyOn(_converse.rosterview, 'update').and.callThrough();
  433. test_utils.openControlBox();
  434. _converse.rosterview.render();
  435. for (var i=0; i<mock.cur_names.length; i++) {
  436. _converse.roster.create({
  437. jid: mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost',
  438. subscription: 'both',
  439. ask: null,
  440. groups: groups,
  441. fullname: mock.cur_names[i]
  442. });
  443. }
  444. test_utils.waitUntil(function () {
  445. return $(_converse.rosterview.el).find('li:visible').length === 30;
  446. }, 600).then(function () {
  447. // Check that usernames appear alphabetically per group
  448. _.each(groups, function (name) {
  449. var $contacts = $(_converse.rosterview.el).find('.roster-group[data-group="'+name+'"] ul li');
  450. var names = $.map($contacts, function (o) { return $(o).text().trim(); });
  451. expect(names).toEqual(_.clone(names).sort());
  452. expect(names.length).toEqual(mock.cur_names.length);
  453. });
  454. done();
  455. });
  456. }));
  457. it("remembers whether it is closed or opened",
  458. mock.initConverse(
  459. null, ['rosterGroupsFetched'], {},
  460. async function (done, _converse) {
  461. _converse.roster_groups = true;
  462. test_utils.openControlBox();
  463. var i=0, j=0;
  464. var groups = {
  465. 'colleagues': 3,
  466. 'friends & acquaintences': 3,
  467. 'Ungrouped': 2
  468. };
  469. _.each(_.keys(groups), function (name) {
  470. j = i;
  471. for (i=j; i<j+groups[name]; i++) {
  472. _converse.roster.create({
  473. jid: mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost',
  474. subscription: 'both',
  475. ask: null,
  476. groups: name === 'ungrouped'? [] : [name],
  477. fullname: mock.cur_names[i]
  478. });
  479. }
  480. });
  481. const view = _converse.rosterview.get('colleagues');
  482. const toggle = view.el.querySelector('a.group-toggle');
  483. expect(view.model.get('state')).toBe('opened');
  484. toggle.click();
  485. await test_utils.waitUntil(() => view.model.get('state') === 'closed');
  486. toggle.click();
  487. await test_utils.waitUntil(() => view.model.get('state') === 'opened');
  488. done();
  489. }));
  490. });
  491. describe("Pending Contacts", function () {
  492. function _addContacts (_converse) {
  493. // Must be initialized, so that render is called and documentFragment set up.
  494. test_utils.createContacts(_converse, 'pending');
  495. test_utils.openControlBox();
  496. }
  497. it("can be collapsed under their own header",
  498. mock.initConverse(
  499. null, ['rosterGroupsFetched'], {},
  500. async function (done, _converse) {
  501. _addContacts(_converse);
  502. await test_utils.waitUntil(() => $(_converse.rosterview.el).find('.roster-group:visible li').length, 1000);
  503. await checkHeaderToggling.apply(
  504. _converse,
  505. [_converse.rosterview.get('Pending contacts').el]
  506. );
  507. done();
  508. }));
  509. it("can be added to the roster",
  510. mock.initConverse(
  511. null, ['rosterGroupsFetched'], {},
  512. function (done, _converse) {
  513. spyOn(_converse.rosterview, 'update').and.callThrough();
  514. test_utils.openControlBox();
  515. _converse.roster.create({
  516. jid: mock.pend_names[0].replace(/ /g,'.').toLowerCase() + '@localhost',
  517. subscription: 'none',
  518. ask: 'subscribe',
  519. fullname: mock.pend_names[0]
  520. });
  521. expect(_converse.rosterview.update).toHaveBeenCalled();
  522. done();
  523. }));
  524. it("are shown in the roster when show_only_online_users",
  525. mock.initConverse(
  526. null, ['rosterGroupsFetched'], {},
  527. async function (done, _converse) {
  528. _converse.show_only_online_users = true;
  529. test_utils.openControlBox();
  530. spyOn(_converse.rosterview, 'update').and.callThrough();
  531. _addContacts(_converse);
  532. await test_utils.waitUntil(() => _.reduce(_converse.rosterview.el.querySelectorAll('li'), (result, el) => result && u.isVisible(el), true), 500);
  533. expect(u.isVisible(_converse.rosterview.el)).toEqual(true);
  534. expect(_converse.rosterview.update).toHaveBeenCalled();
  535. expect(_converse.rosterview.el.querySelectorAll('li').length).toBe(3);
  536. expect(_.filter(_converse.rosterview.el.querySelectorAll('ul.roster-group-contacts'), u.isVisible).length).toBe(1);
  537. done();
  538. }));
  539. it("are shown in the roster when hide_offline_users",
  540. mock.initConverse(
  541. null, ['rosterGroupsFetched'], {},
  542. function (done, _converse) {
  543. _converse.hide_offline_users = true;
  544. spyOn(_converse.rosterview, 'update').and.callThrough();
  545. _addContacts(_converse);
  546. test_utils.waitUntil(function () {
  547. return $(_converse.rosterview.el).find('li:visible').length;
  548. }, 500)
  549. .then(function () {
  550. expect(_converse.rosterview.update).toHaveBeenCalled();
  551. expect($(_converse.rosterview.el).is(':visible')).toBe(true);
  552. expect($(_converse.rosterview.el).find('li:visible').length).toBe(3);
  553. expect($(_converse.rosterview.el).find('ul.roster-group-contacts:visible').length).toBe(1);
  554. done();
  555. });
  556. }));
  557. it("can be removed by the user",
  558. mock.initConverse(
  559. null, ['rosterGroupsFetched'], {},
  560. function (done, _converse) {
  561. _addContacts(_converse);
  562. var name = mock.pend_names[0];
  563. var jid = name.replace(/ /g,'.').toLowerCase() + '@localhost';
  564. var contact = _converse.roster.get(jid);
  565. var sent_IQ;
  566. spyOn(window, 'confirm').and.returnValue(true);
  567. spyOn(contact, 'unauthorize').and.callFake(function () { return contact; });
  568. spyOn(contact, 'removeFromRoster').and.callThrough();
  569. test_utils.waitUntil(function () {
  570. return $(_converse.rosterview.el).find(".pending-contact-name:contains('"+name+"')").length;
  571. }, 700).then(function () {
  572. var sendIQ = _converse.connection.sendIQ;
  573. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  574. sent_IQ = iq;
  575. callback();
  576. });
  577. $(_converse.rosterview.el).find(".pending-contact-name:contains('"+name+"')")
  578. .parent().siblings('.remove-xmpp-contact')[0].click();
  579. return test_utils.waitUntil(function () {
  580. return $(_converse.rosterview.el).find(".pending-contact-name:contains('"+name+"')").length === 0
  581. }, 1000)
  582. }).then(function () {
  583. expect(window.confirm).toHaveBeenCalled();
  584. expect(contact.removeFromRoster).toHaveBeenCalled();
  585. expect(sent_IQ.toLocaleString()).toBe(
  586. `<iq type="set" xmlns="jabber:client">`+
  587. `<query xmlns="jabber:iq:roster">`+
  588. `<item jid="suleyman.van.beusichem@localhost" subscription="remove"/>`+
  589. `</query>`+
  590. `</iq>`);
  591. done();
  592. });
  593. }));
  594. it("do not have a header if there aren't any",
  595. mock.initConverse(
  596. null, ['rosterGroupsFetched'], {},
  597. async function (done, _converse) {
  598. test_utils.openControlBox();
  599. const name = mock.pend_names[0];
  600. _converse.roster.create({
  601. jid: name.replace(/ /g,'.').toLowerCase() + '@localhost',
  602. subscription: 'none',
  603. ask: 'subscribe',
  604. fullname: name
  605. });
  606. spyOn(window, 'confirm').and.returnValue(true);
  607. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback) {
  608. if (typeof callback === "function") { return callback(); }
  609. });
  610. await test_utils.waitUntil(() => {
  611. const el = _converse.rosterview.get('Pending contacts').el;
  612. return u.isVisible(el) && _.filter(el.querySelectorAll('li'), li => u.isVisible(li)).length;
  613. }, 700)
  614. $(_converse.rosterview.el).find(".pending-contact-name:contains('"+name+"')")
  615. .parent().siblings('.remove-xmpp-contact')[0].click();
  616. expect(window.confirm).toHaveBeenCalled();
  617. expect(_converse.connection.sendIQ).toHaveBeenCalled();
  618. await test_utils.waitUntil(() => !u.isVisible(_converse.rosterview.get('Pending contacts').el));
  619. done();
  620. }));
  621. it("is shown when a new private message is received",
  622. mock.initConverse(
  623. null, ['rosterGroupsFetched'], {},
  624. function (done, _converse) {
  625. _addContacts(_converse);
  626. return test_utils.waitUntil(() => _converse.roster.at(0).vcard.get('fullname'))
  627. .then(function () {
  628. var name;
  629. spyOn(window, 'confirm').and.returnValue(true);
  630. for (var i=0; i<mock.pend_names.length; i++) {
  631. name = mock.pend_names[i];
  632. $(_converse.rosterview.el).find(".pending-contact-name:contains('"+name+"')")
  633. .parent().siblings('.remove-xmpp-contact')[0].click();
  634. }
  635. expect($(_converse.rosterview.el).find('#pending-xmpp-contacts').is(':visible')).toBeFalsy();
  636. done();
  637. });
  638. }));
  639. it("can be added to the roster and they will be sorted alphabetically",
  640. mock.initConverse(
  641. null, ['rosterGroupsFetched'], {},
  642. function (done, _converse) {
  643. var i, t;
  644. test_utils.openControlBox();
  645. spyOn(_converse.rosterview, 'update').and.callThrough();
  646. for (i=0; i<mock.pend_names.length; i++) {
  647. _converse.roster.create({
  648. jid: mock.pend_names[i].replace(/ /g,'.').toLowerCase() + '@localhost',
  649. subscription: 'none',
  650. ask: 'subscribe',
  651. fullname: mock.pend_names[i]
  652. });
  653. expect(_converse.rosterview.update).toHaveBeenCalled();
  654. }
  655. return test_utils.waitUntil(function () {
  656. return $(_converse.rosterview.get('Pending contacts').el).find('li:visible').length;
  657. }, 700).then(function () {
  658. // Check that they are sorted alphabetically
  659. t = _.reduce(_converse.rosterview.get('Pending contacts').el.querySelectorAll('.pending-xmpp-contact span'),
  660. function (result, value) {
  661. return result + _.trim(value.textContent);
  662. }, '');
  663. expect(t).toEqual(mock.pend_names.slice(0,i+1).sort().join(''));
  664. done();
  665. });
  666. }));
  667. });
  668. describe("Existing Contacts", function () {
  669. function _addContacts (_converse) {
  670. test_utils.createContacts(_converse, 'current').openControlBox()
  671. }
  672. it("can be collapsed under their own header",
  673. mock.initConverse(
  674. null, ['rosterGroupsFetched'], {},
  675. function (done, _converse) {
  676. _addContacts(_converse);
  677. test_utils.waitUntil(function () {
  678. return $(_converse.rosterview.el).find('li:visible').length;
  679. }, 500).then(function () {
  680. checkHeaderToggling.apply(
  681. _converse,
  682. [_converse.rosterview.el.querySelector('.roster-group')]
  683. ).then(done);
  684. });
  685. }));
  686. it("will be hidden when appearing under a collapsed group",
  687. mock.initConverse(
  688. null, ['rosterGroupsFetched'], {},
  689. function (done, _converse) {
  690. _converse.roster_groups = false;
  691. _addContacts(_converse);
  692. test_utils.waitUntil(function () {
  693. return $(_converse.rosterview.el).find('li:visible').length;
  694. }, 500)
  695. .then(function () {
  696. _converse.rosterview.el.querySelector('.roster-group a.group-toggle').click();
  697. var name = "Max Mustermann";
  698. var jid = name.replace(/ /g,'.').toLowerCase() + '@localhost';
  699. _converse.roster.create({
  700. ask: null,
  701. fullname: name,
  702. jid: jid,
  703. requesting: false,
  704. subscription: 'both'
  705. });
  706. var view = _converse.rosterview.get('My contacts').get(jid);
  707. expect($(view.el).is(':visible')).toBe(false);
  708. done();
  709. });
  710. }));
  711. it("can be added to the roster and they will be sorted alphabetically",
  712. mock.initConverse(
  713. null, ['rosterGroupsFetched'], {},
  714. function (done, _converse) {
  715. test_utils.openControlBox();
  716. spyOn(_converse.rosterview, 'update').and.callThrough();
  717. for (var i=0; i<mock.cur_names.length; i++) {
  718. _converse.roster.create({
  719. jid: mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost',
  720. subscription: 'both',
  721. ask: null,
  722. fullname: mock.cur_names[i]
  723. });
  724. expect(_converse.rosterview.update).toHaveBeenCalled();
  725. }
  726. test_utils.waitUntil(function () {
  727. return $(_converse.rosterview.el).find('li').length;
  728. }, 600).then(function () {
  729. // Check that they are sorted alphabetically
  730. var t = _.reduce($(_converse.rosterview.el.querySelector('.roster-group'))
  731. .find('.current-xmpp-contact.offline a.open-chat'),
  732. function (result, value) {
  733. return result + _.trim(value.textContent);
  734. }, '');
  735. expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
  736. done();
  737. });
  738. }));
  739. it("can be removed by the user",
  740. mock.initConverse(
  741. null, ['rosterGroupsFetched'], {},
  742. async function (done, _converse) {
  743. _addContacts(_converse);
  744. await test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('li').length);
  745. const name = mock.cur_names[0];
  746. const jid = name.replace(/ /g,'.').toLowerCase() + '@localhost';
  747. const contact = _converse.roster.get(jid);
  748. spyOn(window, 'confirm').and.returnValue(true);
  749. spyOn(contact, 'removeFromRoster').and.callThrough();
  750. const sendIQ = _converse.connection.sendIQ;
  751. let sent_IQ;
  752. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  753. sent_IQ = iq;
  754. callback();
  755. });
  756. $(_converse.rosterview.el).find(".open-chat:contains('"+name+"')")
  757. .parent().find('.remove-xmpp-contact')[0].click();
  758. expect(window.confirm).toHaveBeenCalled();
  759. expect(sent_IQ.toLocaleString()).toBe(
  760. `<iq type="set" xmlns="jabber:client">`+
  761. `<query xmlns="jabber:iq:roster"><item jid="max.frankfurter@localhost" subscription="remove"/></query>`+
  762. `</iq>`);
  763. expect(contact.removeFromRoster).toHaveBeenCalled();
  764. await test_utils.waitUntil(() => $(_converse.rosterview.el).find(".open-chat:contains('"+name+"')").length === 0);
  765. done();
  766. }));
  767. it("do not have a header if there aren't any",
  768. mock.initConverse(
  769. null, ['rosterGroupsFetched'], {},
  770. async function (done, _converse) {
  771. test_utils.openControlBox();
  772. var name = mock.cur_names[0];
  773. var contact;
  774. contact = _converse.roster.create({
  775. jid: name.replace(/ /g,'.').toLowerCase() + '@localhost',
  776. subscription: 'both',
  777. ask: null,
  778. fullname: name
  779. });
  780. await test_utils.waitUntil(() => $(_converse.rosterview.el).find('.roster-group:visible li').length, 1000);
  781. spyOn(window, 'confirm').and.returnValue(true);
  782. spyOn(contact, 'removeFromRoster').and.callThrough();
  783. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback) {
  784. if (typeof callback === "function") { return callback(); }
  785. });
  786. expect($(_converse.rosterview.el).find('.roster-group').css('display')).toEqual('block');
  787. $(_converse.rosterview.el).find(".open-chat:contains('"+name+"')")
  788. .parent().find('.remove-xmpp-contact')[0].click();
  789. expect(window.confirm).toHaveBeenCalled();
  790. expect(_converse.connection.sendIQ).toHaveBeenCalled();
  791. expect(contact.removeFromRoster).toHaveBeenCalled();
  792. await test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length === 0);
  793. done();
  794. }));
  795. it("can change their status to online and be sorted alphabetically",
  796. mock.initConverse(
  797. null, ['rosterGroupsFetched'], {},
  798. function (done, _converse) {
  799. _addContacts(_converse);
  800. test_utils.waitUntil(() => $(_converse.rosterview.el).find('.roster-group li').length, 700)
  801. .then(function () {
  802. var jid, t;
  803. spyOn(_converse.rosterview, 'update').and.callThrough();
  804. var $roster = $(_converse.rosterview.el);
  805. for (var i=0; i<mock.cur_names.length; i++) {
  806. jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
  807. _converse.roster.get(jid).presence.set('show', 'online');
  808. expect(_converse.rosterview.update).toHaveBeenCalled();
  809. // Check that they are sorted alphabetically
  810. t = _.reduce($roster.find('.roster-group').find('.current-xmpp-contact.online a.open-chat'), function (result, value) {
  811. return result + _.trim(value.textContent);
  812. }, '');
  813. expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
  814. }
  815. done();
  816. });
  817. }));
  818. it("can change their status to busy and be sorted alphabetically",
  819. mock.initConverse(
  820. null, ['rosterGroupsFetched'], {},
  821. function (done, _converse) {
  822. _addContacts(_converse);
  823. test_utils.waitUntil(function () {
  824. return $(_converse.rosterview.el).find('.roster-group li').length;
  825. }, 700).then(function () {
  826. var jid, t;
  827. spyOn(_converse.rosterview, 'update').and.callThrough();
  828. var $roster = $(_converse.rosterview.el);
  829. for (var i=0; i<mock.cur_names.length; i++) {
  830. jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
  831. _converse.roster.get(jid).presence.set('show', 'dnd');
  832. expect(_converse.rosterview.update).toHaveBeenCalled();
  833. // Check that they are sorted alphabetically
  834. t = _.reduce($roster.find('.roster-group .current-xmpp-contact.dnd a.open-chat'),
  835. function (result, value) {
  836. return result + _.trim(value.textContent);
  837. }, '');
  838. expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
  839. }
  840. done();
  841. });
  842. }));
  843. it("can change their status to away and be sorted alphabetically",
  844. mock.initConverse(
  845. null, ['rosterGroupsFetched'], {},
  846. function (done, _converse) {
  847. _addContacts(_converse);
  848. test_utils.waitUntil(function () {
  849. return $(_converse.rosterview.el).find('.roster-group li').length;
  850. }, 700).then(function () {
  851. var jid, t;
  852. spyOn(_converse.rosterview, 'update').and.callThrough();
  853. var $roster = $(_converse.rosterview.el);
  854. for (var i=0; i<mock.cur_names.length; i++) {
  855. jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
  856. _converse.roster.get(jid).presence.set('show', 'away');
  857. expect(_converse.rosterview.update).toHaveBeenCalled();
  858. // Check that they are sorted alphabetically
  859. t = _.reduce($roster.find('.roster-group .current-xmpp-contact.away a.open-chat'),
  860. function (result, value) {
  861. return result + _.trim(value.textContent);
  862. }, '');
  863. expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
  864. }
  865. done();
  866. });
  867. }));
  868. it("can change their status to xa and be sorted alphabetically",
  869. mock.initConverse(
  870. null, ['rosterGroupsFetched'], {},
  871. function (done, _converse) {
  872. _addContacts(_converse);
  873. test_utils.waitUntil(function () {
  874. return $(_converse.rosterview.el).find('.roster-group li').length;
  875. }, 700).then(function () {
  876. var jid, t;
  877. spyOn(_converse.rosterview, 'update').and.callThrough();
  878. var $roster = $(_converse.rosterview.el);
  879. for (var i=0; i<mock.cur_names.length; i++) {
  880. jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
  881. _converse.roster.get(jid).presence.set('show', 'xa');
  882. expect(_converse.rosterview.update).toHaveBeenCalled();
  883. // Check that they are sorted alphabetically
  884. t = _.reduce($roster.find('.roster-group .current-xmpp-contact.xa a.open-chat'),
  885. function (result, value) {
  886. return result + _.trim(value.textContent);
  887. }, '');
  888. expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
  889. }
  890. done();
  891. });
  892. }));
  893. it("can change their status to unavailable and be sorted alphabetically",
  894. mock.initConverse(
  895. null, ['rosterGroupsFetched'], {},
  896. function (done, _converse) {
  897. _addContacts(_converse);
  898. test_utils.waitUntil(function () {
  899. return $(_converse.rosterview.el).find('.roster-group li').length;
  900. }, 500)
  901. .then(function () {
  902. var jid, t;
  903. spyOn(_converse.rosterview, 'update').and.callThrough();
  904. var $roster = $(_converse.rosterview.el);
  905. for (var i=0; i<mock.cur_names.length; i++) {
  906. jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
  907. _converse.roster.get(jid).presence.set('show', 'unavailable');
  908. expect(_converse.rosterview.update).toHaveBeenCalled();
  909. // Check that they are sorted alphabetically
  910. t = _.reduce($roster.find('.roster-group .current-xmpp-contact.unavailable a.open-chat'),
  911. function (result, value) {
  912. return result + _.trim(value.textContent);
  913. }, '');
  914. expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
  915. }
  916. done();
  917. });
  918. }));
  919. it("are ordered according to status: online, busy, away, xa, unavailable, offline",
  920. mock.initConverse(
  921. null, ['rosterGroupsFetched'], {},
  922. function (done, _converse) {
  923. _addContacts(_converse);
  924. test_utils.waitUntil(function () {
  925. return $(_converse.rosterview.el).find('.roster-group li').length;
  926. }, 700).then(function () {
  927. var i, jid;
  928. for (i=0; i<3; i++) {
  929. jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
  930. _converse.roster.get(jid).presence.set('show', 'online');
  931. }
  932. for (i=3; i<6; i++) {
  933. jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
  934. _converse.roster.get(jid).presence.set('show', 'dnd');
  935. }
  936. for (i=6; i<9; i++) {
  937. jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
  938. _converse.roster.get(jid).presence.set('show', 'away');
  939. }
  940. for (i=9; i<12; i++) {
  941. jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
  942. _converse.roster.get(jid).presence.set('show', 'xa');
  943. }
  944. for (i=12; i<15; i++) {
  945. jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
  946. _converse.roster.get(jid).presence.set('show', 'unavailable');
  947. }
  948. return test_utils.waitUntil(function () {
  949. return $(_converse.rosterview.el).find('li.online').length
  950. })
  951. }).then(function () {
  952. return test_utils.waitUntil(function () {
  953. return $(_converse.rosterview.el).find('li:first').text().trim() === 'Candice van der Knijff'
  954. }, 900);
  955. }).then(function () {
  956. var i;
  957. var contacts = $(_converse.rosterview.el).find('.current-xmpp-contact');
  958. for (i=0; i<3; i++) {
  959. expect($(contacts[i]).hasClass('online')).toBeTruthy();
  960. expect($(contacts[i]).hasClass('both')).toBeTruthy();
  961. expect($(contacts[i]).hasClass('dnd')).toBeFalsy();
  962. expect($(contacts[i]).hasClass('away')).toBeFalsy();
  963. expect($(contacts[i]).hasClass('xa')).toBeFalsy();
  964. expect($(contacts[i]).hasClass('unavailable')).toBeFalsy();
  965. expect($(contacts[i]).hasClass('offline')).toBeFalsy();
  966. }
  967. for (i=3; i<6; i++) {
  968. expect($(contacts[i]).hasClass('dnd')).toBeTruthy();
  969. expect($(contacts[i]).hasClass('both')).toBeTruthy();
  970. expect($(contacts[i]).hasClass('online')).toBeFalsy();
  971. expect($(contacts[i]).hasClass('away')).toBeFalsy();
  972. expect($(contacts[i]).hasClass('xa')).toBeFalsy();
  973. expect($(contacts[i]).hasClass('unavailable')).toBeFalsy();
  974. expect($(contacts[i]).hasClass('offline')).toBeFalsy();
  975. }
  976. for (i=6; i<9; i++) {
  977. expect($(contacts[i]).hasClass('away')).toBeTruthy();
  978. expect($(contacts[i]).hasClass('both')).toBeTruthy();
  979. expect($(contacts[i]).hasClass('online')).toBeFalsy();
  980. expect($(contacts[i]).hasClass('dnd')).toBeFalsy();
  981. expect($(contacts[i]).hasClass('xa')).toBeFalsy();
  982. expect($(contacts[i]).hasClass('unavailable')).toBeFalsy();
  983. expect($(contacts[i]).hasClass('offline')).toBeFalsy();
  984. }
  985. for (i=9; i<12; i++) {
  986. expect($(contacts[i]).hasClass('xa')).toBeTruthy();
  987. expect($(contacts[i]).hasClass('both')).toBeTruthy();
  988. expect($(contacts[i]).hasClass('online')).toBeFalsy();
  989. expect($(contacts[i]).hasClass('dnd')).toBeFalsy();
  990. expect($(contacts[i]).hasClass('away')).toBeFalsy();
  991. expect($(contacts[i]).hasClass('unavailable')).toBeFalsy();
  992. expect($(contacts[i]).hasClass('offline')).toBeFalsy();
  993. }
  994. for (i=12; i<15; i++) {
  995. expect($(contacts[i]).hasClass('unavailable')).toBeTruthy();
  996. expect($(contacts[i]).hasClass('both')).toBeTruthy();
  997. expect($(contacts[i]).hasClass('online')).toBeFalsy();
  998. expect($(contacts[i]).hasClass('dnd')).toBeFalsy();
  999. expect($(contacts[i]).hasClass('away')).toBeFalsy();
  1000. expect($(contacts[i]).hasClass('xa')).toBeFalsy();
  1001. expect($(contacts[i]).hasClass('offline')).toBeFalsy();
  1002. }
  1003. for (i=15; i<mock.cur_names.length; i++) {
  1004. expect($(contacts[i]).hasClass('offline')).toBeTruthy();
  1005. expect($(contacts[i]).hasClass('both')).toBeTruthy();
  1006. expect($(contacts[i]).hasClass('online')).toBeFalsy();
  1007. expect($(contacts[i]).hasClass('dnd')).toBeFalsy();
  1008. expect($(contacts[i]).hasClass('away')).toBeFalsy();
  1009. expect($(contacts[i]).hasClass('xa')).toBeFalsy();
  1010. expect($(contacts[i]).hasClass('unavailable')).toBeFalsy();
  1011. }
  1012. done();
  1013. });
  1014. }));
  1015. });
  1016. describe("Requesting Contacts", function () {
  1017. it("can be added to the roster and they will be sorted alphabetically",
  1018. mock.initConverse(
  1019. null, ['rosterGroupsFetched'], {},
  1020. function (done, _converse) {
  1021. var i, children;
  1022. var names = [];
  1023. var addName = function (item) {
  1024. if (!$(item).hasClass('request-actions')) {
  1025. names.push($(item).text().replace(/^\s+|\s+$/g, ''));
  1026. }
  1027. };
  1028. spyOn(_converse.rosterview, 'update').and.callThrough();
  1029. spyOn(_converse.controlboxtoggle, 'showControlBox').and.callThrough();
  1030. for (i=0; i<mock.req_names.length; i++) {
  1031. _converse.roster.create({
  1032. jid: mock.req_names[i].replace(/ /g,'.').toLowerCase() + '@localhost',
  1033. subscription: 'none',
  1034. ask: null,
  1035. requesting: true,
  1036. fullname: mock.req_names[i]
  1037. });
  1038. }
  1039. test_utils.waitUntil(function () {
  1040. return _converse.rosterview.get('Contact requests').el.querySelectorAll('li').length;
  1041. }, 700).then(function () {
  1042. expect(_converse.rosterview.update).toHaveBeenCalled();
  1043. // Check that they are sorted alphabetically
  1044. children = _converse.rosterview.get('Contact requests').el.querySelectorAll('.requesting-xmpp-contact span');
  1045. names = [];
  1046. _.each(children, addName);
  1047. expect(names.join('')).toEqual(mock.req_names.slice(0,mock.req_names.length+1).sort().join(''));
  1048. done();
  1049. });
  1050. }));
  1051. it("do not have a header if there aren't any",
  1052. mock.initConverse(
  1053. null, ['rosterGroupsFetched'], {},
  1054. function (done, _converse) {
  1055. test_utils.openControlBox();
  1056. var name = mock.req_names[0];
  1057. spyOn(window, 'confirm').and.returnValue(true);
  1058. _converse.roster.create({
  1059. jid: name.replace(/ /g,'.').toLowerCase() + '@localhost',
  1060. subscription: 'none',
  1061. ask: null,
  1062. requesting: true,
  1063. fullname: name
  1064. });
  1065. test_utils.waitUntil(function () {
  1066. return $(_converse.rosterview.el).find('.roster-group:visible li').length;
  1067. }, 900).then(function () {
  1068. expect(u.isVisible(_converse.rosterview.get('Contact requests').el)).toEqual(true);
  1069. expect($(_converse.rosterview.el).find('.roster-group:visible li').length).toBe(1);
  1070. $(_converse.rosterview.el).find('.roster-group:visible li .decline-xmpp-request')[0].click();
  1071. expect(window.confirm).toHaveBeenCalled();
  1072. expect(u.isVisible(_converse.rosterview.get('Contact requests').el)).toEqual(false);
  1073. done();
  1074. });
  1075. }));
  1076. it("can be collapsed under their own header",
  1077. mock.initConverse(
  1078. null, ['rosterGroupsFetched'], {},
  1079. function (done, _converse) {
  1080. test_utils.createContacts(_converse, 'requesting').openControlBox();
  1081. test_utils.waitUntil(function () {
  1082. return $(_converse.rosterview.el).find('.roster-group:visible li').length;
  1083. }, 700).then(function () {
  1084. checkHeaderToggling.apply(
  1085. _converse,
  1086. [_converse.rosterview.get('Contact requests').el]
  1087. ).then(done);
  1088. });
  1089. }));
  1090. it("can have their requests accepted by the user",
  1091. mock.initConverse(
  1092. null, ['rosterGroupsFetched'], {},
  1093. async function (done, _converse) {
  1094. test_utils.openControlBox();
  1095. test_utils.createContacts(_converse, 'requesting').openControlBox();
  1096. await test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length)
  1097. // TODO: Testing can be more thorough here, the user is
  1098. // actually not accepted/authorized because of
  1099. // mock_connection.
  1100. const jid = mock.req_names.sort()[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  1101. const contact = _converse.roster.get(jid);
  1102. spyOn(contact, 'authorize').and.callFake(() => contact);
  1103. spyOn(_converse.roster, 'sendContactAddIQ').and.callFake(() => Promise.resolve());
  1104. $(_converse.rosterview.el).find(".req-contact-name:contains('"+name+"')")
  1105. .parent().parent().find('.accept-xmpp-request')[0].click();
  1106. expect(_converse.roster.sendContactAddIQ).toHaveBeenCalled();
  1107. await test_utils.waitUntil(() => contact.authorize.calls.count());
  1108. expect(contact.authorize).toHaveBeenCalled();
  1109. done();
  1110. }));
  1111. it("can have their requests denied by the user",
  1112. mock.initConverse(
  1113. null, ['rosterGroupsFetched'], {},
  1114. function (done, _converse) {
  1115. test_utils.createContacts(_converse, 'requesting').openControlBox();
  1116. test_utils.waitUntil(function () {
  1117. return $(_converse.rosterview.el).find('.roster-group li').length;
  1118. }, 700).then(function () {
  1119. _converse.rosterview.update(); // XXX: Hack to make sure $roster element is attaced.
  1120. var name = mock.req_names.sort()[1];
  1121. var jid = name.replace(/ /g,'.').toLowerCase() + '@localhost';
  1122. var contact = _converse.roster.get(jid);
  1123. spyOn(window, 'confirm').and.returnValue(true);
  1124. spyOn(contact, 'unauthorize').and.callFake(function () { return contact; });
  1125. $(_converse.rosterview.el).find(".req-contact-name:contains('"+name+"')")
  1126. .parent().parent().find('.decline-xmpp-request')[0].click();
  1127. expect(window.confirm).toHaveBeenCalled();
  1128. expect(contact.unauthorize).toHaveBeenCalled();
  1129. // There should now be one less contact
  1130. expect(_converse.roster.length).toEqual(mock.req_names.length-1);
  1131. done();
  1132. });
  1133. }));
  1134. it("are persisted even if other contacts' change their presence ", mock.initConverse(
  1135. null, ['rosterGroupsFetched'], {}, function (done, _converse) {
  1136. /* This is a regression test.
  1137. * https://github.com/jcbrand/_converse.js/issues/262
  1138. */
  1139. expect(_converse.roster.pluck('jid').length).toBe(0);
  1140. let stanza = $pres({from: 'data@enterprise/resource', type: 'subscribe'});
  1141. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  1142. test_utils.waitUntil(function () {
  1143. return $('a:contains("Contact requests")').length;
  1144. }, 700).then(function () {
  1145. expect(_converse.roster.pluck('jid').length).toBe(1);
  1146. expect(_.includes(_converse.roster.pluck('jid'), 'data@enterprise')).toBeTruthy();
  1147. // Taken from the spec
  1148. // https://xmpp.org/rfcs/rfc3921.html#rfc.section.7.3
  1149. stanza = $iq({
  1150. to: _converse.connection.jid,
  1151. type: 'result',
  1152. id: 'roster_1'
  1153. }).c('query', {
  1154. xmlns: 'jabber:iq:roster',
  1155. }).c('item', {
  1156. jid: 'romeo@example.net',
  1157. name: 'Romeo',
  1158. subscription:'both'
  1159. }).c('group').t('Friends').up().up()
  1160. .c('item', {
  1161. jid: 'mercutio@example.org',
  1162. name: 'Mercutio',
  1163. subscription:'from'
  1164. }).c('group').t('Friends').up().up()
  1165. .c('item', {
  1166. jid: 'benvolio@example.org',
  1167. name: 'Benvolio',
  1168. subscription:'both'
  1169. }).c('group').t('Friends');
  1170. _converse.roster.onReceivedFromServer(stanza.tree());
  1171. expect(_.includes(_converse.roster.pluck('jid'), 'data@enterprise')).toBeTruthy();
  1172. done();
  1173. });
  1174. }));
  1175. });
  1176. describe("All Contacts", function () {
  1177. it("are saved to, and can be retrieved from browserStorage",
  1178. mock.initConverse(
  1179. null, ['rosterGroupsFetched'], {},
  1180. function (done, _converse) {
  1181. test_utils.createContacts(_converse, 'all').openControlBox();
  1182. var new_attrs, old_attrs, attrs;
  1183. var num_contacts = _converse.roster.length;
  1184. var new_roster = new _converse.RosterContacts();
  1185. // Roster items are yet to be fetched from browserStorage
  1186. expect(new_roster.length).toEqual(0);
  1187. new_roster.browserStorage = _converse.roster.browserStorage;
  1188. new_roster.fetch();
  1189. expect(new_roster.length).toEqual(num_contacts);
  1190. // Check that the roster items retrieved from browserStorage
  1191. // have the same attributes values as the original ones.
  1192. attrs = ['jid', 'fullname', 'subscription', 'ask'];
  1193. for (var i=0; i<attrs.length; i++) {
  1194. new_attrs = _.map(_.map(new_roster.models, 'attributes'), attrs[i]);
  1195. old_attrs = _.map(_.map(_converse.roster.models, 'attributes'), attrs[i]);
  1196. // Roster items in storage are not necessarily sorted,
  1197. // so we have to sort them here to do a proper
  1198. // comparison
  1199. expect(_.isEqual(new_attrs.sort(), old_attrs.sort())).toEqual(true);
  1200. }
  1201. done();
  1202. }));
  1203. it("will show fullname and jid properties on tooltip",
  1204. mock.initConverse(
  1205. null, ['rosterGroupsFetched'], {},
  1206. function (done, _converse) {
  1207. test_utils.createContacts(_converse, 'all').openControlBox();
  1208. test_utils.waitUntil(function () {
  1209. return $(_converse.rosterview.el).find('.roster-group li').length;
  1210. }, 700).then(function () {
  1211. var jid, name, i;
  1212. for (i=0; i<mock.cur_names.length; i++) {
  1213. name = mock.cur_names[i];
  1214. jid = name.replace(/ /g,'.').toLowerCase() + '@localhost';
  1215. var $dd = $(_converse.rosterview.el).find("li:contains('"+name+"')").children().first();
  1216. var dd_text = $dd.text();
  1217. var dd_title = $dd.attr('title');
  1218. expect(_.trim(dd_text)).toBe(name);
  1219. expect(dd_title).toContain(name);
  1220. expect(dd_title).toContain(jid);
  1221. }
  1222. done();
  1223. });
  1224. }));
  1225. });
  1226. });
  1227. }));