roster.js 64 KB

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