roster.js 66 KB

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