roster.js 66 KB

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