roster.js 64 KB

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