roster.js 68 KB

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