roster.js 71 KB

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