roster.js 68 KB

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