chatbox.js 85 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624
  1. /*global mock */
  2. const _ = converse.env._;
  3. const $msg = converse.env.$msg;
  4. const Strophe = converse.env.Strophe;
  5. const u = converse.env.utils;
  6. const sizzle = converse.env.sizzle;
  7. describe("Chatboxes", function () {
  8. describe("A Chatbox", function () {
  9. it("has a /help command to show the available commands", mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) {
  10. await mock.waitForRoster(_converse, 'current', 1);
  11. await mock.openControlBox(_converse);
  12. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  13. await mock.openChatBoxFor(_converse, contact_jid);
  14. const view = _converse.chatboxviews.get(contact_jid);
  15. mock.sendMessage(view, '/help');
  16. const info_messages = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info:not(.chat-date)'), 0);
  17. expect(info_messages.length).toBe(4);
  18. expect(info_messages.pop().textContent).toBe('/help: Show this menu');
  19. expect(info_messages.pop().textContent).toBe('/me: Write in the third person');
  20. expect(info_messages.pop().textContent).toBe('/close: Close this chat');
  21. expect(info_messages.pop().textContent).toBe('/clear: Remove messages');
  22. const msg = $msg({
  23. from: contact_jid,
  24. to: _converse.connection.jid,
  25. type: 'chat',
  26. id: u.getUniqueId()
  27. }).c('body').t('hello world').tree();
  28. await _converse.handleMessageStanza(msg);
  29. await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
  30. expect(view.msgs_container.lastElementChild.textContent.trim().indexOf('hello world')).not.toBe(-1);
  31. done();
  32. }));
  33. it("supports the /me command", mock.initConverse(['rosterGroupsFetched'], {}, async function (done, _converse) {
  34. await mock.waitForRoster(_converse, 'current');
  35. await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
  36. await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
  37. await mock.openControlBox(_converse);
  38. expect(_converse.chatboxes.length).toEqual(1);
  39. const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  40. let message = '/me is tired';
  41. const msg = $msg({
  42. from: sender_jid,
  43. to: _converse.connection.jid,
  44. type: 'chat',
  45. id: u.getUniqueId()
  46. }).c('body').t(message).up()
  47. .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
  48. await _converse.handleMessageStanza(msg);
  49. const view = _converse.chatboxviews.get(sender_jid);
  50. await new Promise(resolve => view.once('messageInserted', resolve));
  51. expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(1);
  52. expect(_.includes(view.el.querySelector('.chat-msg__author').textContent, '**Mercutio')).toBeTruthy();
  53. expect(view.el.querySelector('.chat-msg__text').textContent).toBe('is tired');
  54. message = '/me is as well';
  55. await mock.sendMessage(view, message);
  56. expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(2);
  57. await u.waitUntil(() => sizzle('.chat-msg__author:last', view.el).pop().textContent.trim() === '**Romeo Montague');
  58. const last_el = sizzle('.chat-msg__text:last', view.el).pop();
  59. expect(last_el.textContent).toBe('is as well');
  60. expect(u.hasClass('chat-msg--followup', last_el)).toBe(false);
  61. // Check that /me messages after a normal message don't
  62. // get the 'chat-msg--followup' class.
  63. message = 'This a normal message';
  64. await mock.sendMessage(view, message);
  65. let message_el = view.el.querySelector('.message:last-child');
  66. expect(u.hasClass('chat-msg--followup', message_el)).toBeFalsy();
  67. message = '/me wrote a 3rd person message';
  68. await mock.sendMessage(view, message);
  69. message_el = view.el.querySelector('.message:last-child');
  70. expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(3);
  71. expect(sizzle('.chat-msg__text:last', view.el).pop().textContent).toBe('wrote a 3rd person message');
  72. expect(u.isVisible(sizzle('.chat-msg__author:last', view.el).pop())).toBeTruthy();
  73. expect(u.hasClass('chat-msg--followup', message_el)).toBeFalsy();
  74. done();
  75. }));
  76. it("is created when you click on a roster item", mock.initConverse(
  77. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  78. async function (done, _converse) {
  79. await mock.waitForRoster(_converse, 'current');
  80. await mock.openControlBox(_converse);
  81. // openControlBox was called earlier, so the controlbox is
  82. // visible, but no other chat boxes have been created.
  83. expect(_converse.chatboxes.length).toEqual(1);
  84. spyOn(_converse.chatboxviews, 'trimChats');
  85. expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(1); // Controlbox is open
  86. await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length, 700);
  87. const online_contacts = _converse.rosterview.el.querySelectorAll('.roster-group .current-xmpp-contact a.open-chat');
  88. expect(online_contacts.length).toBe(17);
  89. let el = online_contacts[0];
  90. el.click();
  91. await u.waitUntil(() => document.querySelectorAll("#conversejs .chatbox").length == 2);
  92. expect(_converse.chatboxviews.trimChats).toHaveBeenCalled();
  93. online_contacts[1].click();
  94. await u.waitUntil(() => _converse.chatboxes.length == 3);
  95. el = online_contacts[1];
  96. expect(_converse.chatboxviews.trimChats).toHaveBeenCalled();
  97. // Check that new chat boxes are created to the left of the
  98. // controlbox (but to the right of all existing chat boxes)
  99. expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(3);
  100. done();
  101. }));
  102. it("opens when a new message is received", mock.initConverse(
  103. ['rosterGroupsFetched'], {'allow_non_roster_messaging': true},
  104. async function (done, _converse) {
  105. await mock.waitForRoster(_converse, 'current', 0);
  106. const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  107. const stanza = u.toStanza(`
  108. <message from="${sender_jid}"
  109. type="chat"
  110. to="romeo@montague.lit/orchard">
  111. <body>Hey\nHave you heard the news?</body>
  112. </message>`);
  113. const message_promise = new Promise(resolve => _converse.api.listen.on('message', resolve));
  114. _converse.connection._dataRecv(mock.createRequest(stanza));
  115. await new Promise(resolve => _converse.api.listen.once('chatBoxViewInitialized', resolve));
  116. await u.waitUntil(() => message_promise);
  117. expect(_converse.chatboxviews.keys().length).toBe(2);
  118. done();
  119. }));
  120. it("doesn't open when a message without body is received", mock.initConverse(
  121. ['rosterGroupsFetched'], {},
  122. async function (done, _converse) {
  123. await mock.waitForRoster(_converse, 'current', 1);
  124. const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  125. const stanza = u.toStanza(`
  126. <message from="${sender_jid}"
  127. type="chat"
  128. to="romeo@montague.lit/orchard">
  129. <composing xmlns="http://jabber.org/protocol/chatstates"/>
  130. </message>`);
  131. const message_promise = new Promise(resolve => _converse.api.listen.on('message', resolve))
  132. _converse.connection._dataRecv(mock.createRequest(stanza));
  133. await u.waitUntil(() => message_promise);
  134. expect(_converse.chatboxviews.keys().length).toBe(1);
  135. done();
  136. }));
  137. it("can be trimmed to conserve space",
  138. mock.initConverse(['rosterGroupsFetched'], {},
  139. async function (done, _converse) {
  140. spyOn(_converse.chatboxviews, 'trimChats');
  141. const trimmed_chatboxes = _converse.minimized_chats;
  142. spyOn(trimmed_chatboxes, 'addChat').and.callThrough();
  143. spyOn(trimmed_chatboxes, 'removeChat').and.callThrough();
  144. await mock.waitForRoster(_converse, 'current');
  145. await mock.openControlBox(_converse);
  146. expect(_converse.chatboxviews.trimChats.calls.count()).toBe(1);
  147. let jid, chatboxview;
  148. // openControlBox was called earlier, so the controlbox is
  149. // visible, but no other chat boxes have been created.
  150. expect(_converse.chatboxes.length).toEqual(1);
  151. expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(1); // Controlbox is open
  152. _converse.rosterview.update(); // XXX: Hack to make sure $roster element is attached.
  153. await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length);
  154. // Test that they can be maximized again
  155. const online_contacts = _converse.rosterview.el.querySelectorAll('.roster-group .current-xmpp-contact a.open-chat');
  156. expect(online_contacts.length).toBe(17);
  157. let i;
  158. for (i=0; i<online_contacts.length; i++) {
  159. const el = online_contacts[i];
  160. el.click();
  161. }
  162. await u.waitUntil(() => _converse.chatboxes.length == 16);
  163. expect(_converse.chatboxviews.trimChats.calls.count()).toBe(16);
  164. _converse.api.chatviews.get().forEach(v => spyOn(v, 'onMinimized').and.callThrough());
  165. for (i=0; i<online_contacts.length; i++) {
  166. const el = online_contacts[i];
  167. jid = _.trim(el.textContent.trim()).replace(/ /g,'.').toLowerCase() + '@montague.lit';
  168. chatboxview = _converse.chatboxviews.get(jid);
  169. chatboxview.model.set({'minimized': true});
  170. expect(trimmed_chatboxes.addChat).toHaveBeenCalled();
  171. expect(chatboxview.onMinimized).toHaveBeenCalled();
  172. }
  173. await u.waitUntil(() => _converse.chatboxviews.keys().length);
  174. var key = _converse.chatboxviews.keys()[1];
  175. const trimmedview = trimmed_chatboxes.get(key);
  176. const chatbox = trimmedview.model;
  177. spyOn(chatbox, 'maximize').and.callThrough();
  178. spyOn(trimmedview, 'restore').and.callThrough();
  179. trimmedview.delegateEvents();
  180. trimmedview.el.querySelector("a.restore-chat").click();
  181. expect(trimmedview.restore).toHaveBeenCalled();
  182. expect(chatbox.maximize).toHaveBeenCalled();
  183. expect(_converse.chatboxviews.trimChats.calls.count()).toBe(17);
  184. done();
  185. }));
  186. it("can be opened in minimized mode initially",
  187. mock.initConverse(
  188. ['rosterGroupsFetched'], {},
  189. async function (done, _converse) {
  190. await mock.waitForRoster(_converse, 'current');
  191. const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  192. await _converse.api.chats.create(sender_jid, {'minimized': true});
  193. await u.waitUntil(() => _converse.chatboxes.length > 1);
  194. const chatBoxView = _converse.chatboxviews.get(sender_jid);
  195. expect(u.isVisible(chatBoxView.el)).toBeFalsy();
  196. const minimized_chat = _converse.minimized_chats.get(sender_jid);
  197. expect(minimized_chat).toBeTruthy();
  198. expect(u.isVisible(minimized_chat.el)).toBeTruthy();
  199. done();
  200. }));
  201. it("is focused if its already open and you click on its corresponding roster item",
  202. mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
  203. async function (done, _converse) {
  204. await mock.waitForRoster(_converse, 'current');
  205. await mock.openControlBox(_converse);
  206. expect(_converse.chatboxes.length).toEqual(1);
  207. const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  208. const view = await mock.openChatBoxFor(_converse, contact_jid);
  209. const el = sizzle('a.open-chat:contains("'+view.model.getDisplayName()+'")', _converse.rosterview.el).pop();
  210. await u.waitUntil(() => u.isVisible(el));
  211. const textarea = view.el.querySelector('.chat-textarea');
  212. await u.waitUntil(() => u.isVisible(textarea));
  213. textarea.blur();
  214. spyOn(view.model, 'maybeShow').and.callThrough();
  215. spyOn(view, 'focus').and.callThrough();
  216. el.click();
  217. await u.waitUntil(() => view.model.maybeShow.calls.count(), 1000);
  218. expect(view.model.maybeShow).toHaveBeenCalled();
  219. expect(view.focus).toHaveBeenCalled();
  220. expect(_converse.chatboxes.length).toEqual(2);
  221. done();
  222. }));
  223. it("can be saved to, and retrieved from, browserStorage",
  224. mock.initConverse(
  225. ['rosterGroupsFetched'], {},
  226. async function (done, _converse) {
  227. spyOn(_converse.ChatBoxViews.prototype, 'trimChats');
  228. await mock.waitForRoster(_converse, 'current');
  229. await mock.openControlBox(_converse);
  230. spyOn(_converse.api, "trigger").and.callThrough();
  231. mock.openChatBoxes(_converse, 6);
  232. await u.waitUntil(() => _converse.chatboxes.length == 7);
  233. expect(_converse.chatboxviews.trimChats).toHaveBeenCalled();
  234. // We instantiate a new ChatBoxes collection, which by default
  235. // will be empty.
  236. const newchatboxes = new _converse.ChatBoxes();
  237. expect(newchatboxes.length).toEqual(0);
  238. // The chatboxes will then be fetched from browserStorage inside the
  239. // onConnected method
  240. newchatboxes.onConnected();
  241. await new Promise(resolve => _converse.api.listen.on('chatBoxesFetched', resolve));
  242. expect(newchatboxes.length).toEqual(7);
  243. // Check that the chatboxes items retrieved from browserStorage
  244. // have the same attributes values as the original ones.
  245. const attrs = ['id', 'box_id', 'visible'];
  246. let new_attrs, old_attrs;
  247. for (var i=0; i<attrs.length; i++) {
  248. new_attrs = _.map(_.map(newchatboxes.models, 'attributes'), attrs[i]);
  249. old_attrs = _.map(_.map(_converse.chatboxes.models, 'attributes'), attrs[i]);
  250. expect(_.isEqual(new_attrs, old_attrs)).toEqual(true);
  251. }
  252. _converse.rosterview.render();
  253. done();
  254. }));
  255. it("can be closed by clicking a DOM element with class 'close-chatbox-button'",
  256. mock.initConverse(
  257. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  258. async function (done, _converse) {
  259. await mock.waitForRoster(_converse, 'current');
  260. await mock.openControlBox(_converse);
  261. const contact_jid = mock.cur_names[7].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  262. await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
  263. await mock.openChatBoxFor(_converse, contact_jid);
  264. const controlview = _converse.chatboxviews.get('controlbox'), // The controlbox is currently open
  265. chatview = _converse.chatboxviews.get(contact_jid);
  266. spyOn(chatview, 'close').and.callThrough();
  267. spyOn(controlview, 'close').and.callThrough();
  268. spyOn(_converse.api, "trigger").and.callThrough();
  269. // We need to rebind all events otherwise our spy won't be called
  270. controlview.delegateEvents();
  271. chatview.delegateEvents();
  272. controlview.el.querySelector('.close-chatbox-button').click();
  273. expect(controlview.close).toHaveBeenCalled();
  274. await new Promise(resolve => _converse.api.listen.once('chatBoxClosed', resolve));
  275. expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object));
  276. chatview.el.querySelector('.close-chatbox-button').click();
  277. expect(chatview.close).toHaveBeenCalled();
  278. await new Promise(resolve => _converse.api.listen.once('chatBoxClosed', resolve));
  279. expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object));
  280. done();
  281. }));
  282. it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'",
  283. mock.initConverse(
  284. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  285. async function (done, _converse) {
  286. await mock.waitForRoster(_converse, 'current');
  287. await mock.openControlBox(_converse);
  288. const contact_jid = mock.cur_names[7].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  289. await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
  290. await mock.openChatBoxFor(_converse, contact_jid);
  291. const trimmed_chatboxes = _converse.minimized_chats;
  292. const chatview = _converse.chatboxviews.get(contact_jid);
  293. spyOn(chatview, 'minimize').and.callThrough();
  294. spyOn(_converse.api, "trigger").and.callThrough();
  295. // We need to rebind all events otherwise our spy won't be called
  296. chatview.delegateEvents();
  297. chatview.el.querySelector('.toggle-chatbox-button').click();
  298. expect(chatview.minimize).toHaveBeenCalled();
  299. expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object));
  300. expect(_converse.api.trigger.calls.count(), 2);
  301. expect(u.isVisible(chatview.el)).toBeFalsy();
  302. expect(chatview.model.get('minimized')).toBeTruthy();
  303. chatview.el.querySelector('.toggle-chatbox-button').click();
  304. const trimmedview = trimmed_chatboxes.get(chatview.model.get('id'));
  305. spyOn(trimmedview, 'restore').and.callThrough();
  306. trimmedview.delegateEvents();
  307. trimmedview.el.querySelector("a.restore-chat").click();
  308. expect(trimmedview.restore).toHaveBeenCalled();
  309. expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
  310. expect(chatview.model.get('minimized')).toBeFalsy();
  311. done();
  312. }));
  313. it("will be removed from browserStorage when closed",
  314. mock.initConverse(
  315. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  316. async function (done, _converse) {
  317. spyOn(_converse.ChatBoxViews.prototype, 'trimChats');
  318. await mock.waitForRoster(_converse, 'current');
  319. await mock.openControlBox(_converse);
  320. await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
  321. spyOn(_converse.api, "trigger").and.callThrough();
  322. mock.closeControlBox();
  323. await new Promise(resolve => _converse.api.listen.once('chatBoxClosed', resolve));
  324. expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object));
  325. expect(_converse.chatboxes.length).toEqual(1);
  326. expect(_converse.chatboxes.pluck('id')).toEqual(['controlbox']);
  327. mock.openChatBoxes(_converse, 6);
  328. await u.waitUntil(() => _converse.chatboxes.length == 7)
  329. expect(_converse.chatboxviews.trimChats).toHaveBeenCalled();
  330. expect(_converse.chatboxes.length).toEqual(7);
  331. expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxViewInitialized', jasmine.any(Object));
  332. await mock.closeAllChatBoxes(_converse);
  333. expect(_converse.chatboxes.length).toEqual(1);
  334. expect(_converse.chatboxes.pluck('id')).toEqual(['controlbox']);
  335. expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object));
  336. const newchatboxes = new _converse.ChatBoxes();
  337. expect(newchatboxes.length).toEqual(0);
  338. expect(_converse.chatboxes.pluck('id')).toEqual(['controlbox']);
  339. // onConnected will fetch chatboxes in browserStorage, but
  340. // because there aren't any open chatboxes, there won't be any
  341. // in browserStorage either. XXX except for the controlbox
  342. newchatboxes.onConnected();
  343. await new Promise(resolve => _converse.api.listen.on('chatBoxesFetched', resolve));
  344. expect(newchatboxes.length).toEqual(1);
  345. expect(newchatboxes.models[0].id).toBe("controlbox");
  346. done();
  347. }));
  348. describe("A chat toolbar", function () {
  349. it("can be found on each chat box",
  350. mock.initConverse(
  351. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  352. async function (done, _converse) {
  353. await mock.waitForRoster(_converse, 'current', 3);
  354. await mock.openControlBox(_converse);
  355. const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  356. await mock.openChatBoxFor(_converse, contact_jid);
  357. const chatbox = _converse.chatboxes.get(contact_jid);
  358. const view = _converse.chatboxviews.get(contact_jid);
  359. expect(chatbox).toBeDefined();
  360. expect(view).toBeDefined();
  361. const toolbar = view.el.querySelector('ul.chat-toolbar');
  362. expect(_.isElement(toolbar)).toBe(true);
  363. expect(toolbar.querySelectorAll(':scope > li').length).toBe(2);
  364. done();
  365. }));
  366. it("shows the remaining character count if a message_limit is configured",
  367. mock.initConverse(
  368. ['rosterGroupsFetched', 'chatBoxesFetched'], {'message_limit': 200},
  369. async function (done, _converse) {
  370. await mock.waitForRoster(_converse, 'current', 3);
  371. await mock.openControlBox(_converse);
  372. const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  373. await mock.openChatBoxFor(_converse, contact_jid);
  374. const view = _converse.chatboxviews.get(contact_jid);
  375. const toolbar = view.el.querySelector('.chat-toolbar');
  376. const counter = toolbar.querySelector('.message-limit');
  377. expect(counter.textContent).toBe('200');
  378. view.insertIntoTextArea('hello world');
  379. expect(counter.textContent).toBe('188');
  380. toolbar.querySelector('a.toggle-smiley').click();
  381. const picker = await u.waitUntil(() => view.el.querySelector('.emoji-picker__lists'));
  382. const item = await u.waitUntil(() => picker.querySelector('.emoji-picker li.insert-emoji a'));
  383. item.click()
  384. expect(counter.textContent).toBe('179');
  385. const textarea = view.el.querySelector('.chat-textarea');
  386. const ev = {
  387. target: textarea,
  388. preventDefault: function preventDefault () {},
  389. keyCode: 13 // Enter
  390. };
  391. view.onKeyDown(ev);
  392. await new Promise(resolve => view.once('messageInserted', resolve));
  393. view.onKeyUp(ev);
  394. expect(counter.textContent).toBe('200');
  395. textarea.value = 'hello world';
  396. view.onKeyUp(ev);
  397. expect(counter.textContent).toBe('189');
  398. done();
  399. }));
  400. it("does not show a remaining character count if message_limit is zero",
  401. mock.initConverse(
  402. ['rosterGroupsFetched', 'chatBoxesFetched'], {'message_limit': 0},
  403. async function (done, _converse) {
  404. await mock.waitForRoster(_converse, 'current', 3);
  405. await mock.openControlBox(_converse);
  406. const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  407. await mock.openChatBoxFor(_converse, contact_jid);
  408. const view = _converse.chatboxviews.get(contact_jid);
  409. const counter = view.el.querySelector('.chat-toolbar .message-limit');
  410. expect(counter).toBe(null);
  411. done();
  412. }));
  413. it("can contain a button for starting a call",
  414. mock.initConverse(
  415. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  416. async function (done, _converse) {
  417. await mock.waitForRoster(_converse, 'current');
  418. await mock.openControlBox(_converse);
  419. let toolbar, call_button;
  420. const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  421. spyOn(_converse.api, "trigger").and.callThrough();
  422. // First check that the button doesn't show if it's not enabled
  423. // via "visible_toolbar_buttons"
  424. _converse.visible_toolbar_buttons.call = false;
  425. await mock.openChatBoxFor(_converse, contact_jid);
  426. let view = _converse.chatboxviews.get(contact_jid);
  427. toolbar = view.el.querySelector('ul.chat-toolbar');
  428. call_button = toolbar.querySelector('.toggle-call');
  429. expect(call_button === null).toBeTruthy();
  430. view.close();
  431. // Now check that it's shown if enabled and that it emits
  432. // callButtonClicked
  433. _converse.visible_toolbar_buttons.call = true; // enable the button
  434. await mock.openChatBoxFor(_converse, contact_jid);
  435. view = _converse.chatboxviews.get(contact_jid);
  436. toolbar = view.el.querySelector('ul.chat-toolbar');
  437. call_button = toolbar.querySelector('.toggle-call');
  438. call_button.click();
  439. expect(_converse.api.trigger).toHaveBeenCalledWith('callButtonClicked', jasmine.any(Object));
  440. done();
  441. }));
  442. });
  443. describe("A Chat Status Notification", function () {
  444. it("does not open a new chatbox",
  445. mock.initConverse(
  446. ['rosterGroupsFetched'], {},
  447. async function (done, _converse) {
  448. await mock.waitForRoster(_converse, 'current');
  449. await mock.openControlBox(_converse);
  450. const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  451. // <composing> state
  452. const stanza = $msg({
  453. 'from': sender_jid,
  454. 'to': _converse.connection.jid,
  455. 'type': 'chat',
  456. 'id': u.getUniqueId()
  457. }).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
  458. spyOn(_converse.api, "trigger").and.callThrough();
  459. _converse.connection._dataRecv(mock.createRequest(stanza));
  460. await u.waitUntil(() => _converse.api.trigger.calls.count());
  461. expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
  462. expect(_converse.chatboxviews.keys().length).toBe(1);
  463. done();
  464. }));
  465. describe("An active notification", function () {
  466. it("is sent when the user opens a chat box",
  467. mock.initConverse(
  468. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  469. async function (done, _converse) {
  470. await mock.waitForRoster(_converse, 'current');
  471. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  472. await mock.openControlBox(_converse);
  473. u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
  474. spyOn(_converse.connection, 'send');
  475. await mock.openChatBoxFor(_converse, contact_jid);
  476. const view = _converse.chatboxviews.get(contact_jid);
  477. expect(view.model.get('chat_state')).toBe('active');
  478. expect(_converse.connection.send).toHaveBeenCalled();
  479. const stanza = _converse.connection.send.calls.argsFor(0)[0].tree();
  480. expect(stanza.getAttribute('to')).toBe(contact_jid);
  481. expect(stanza.childNodes.length).toBe(3);
  482. expect(stanza.childNodes[0].tagName).toBe('active');
  483. expect(stanza.childNodes[1].tagName).toBe('no-store');
  484. expect(stanza.childNodes[2].tagName).toBe('no-permanent-store');
  485. done();
  486. }));
  487. it("is sent when the user maximizes a minimized a chat box", mock.initConverse(
  488. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  489. async function (done, _converse) {
  490. await mock.waitForRoster(_converse, 'current', 1);
  491. await mock.openControlBox(_converse);
  492. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  493. await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
  494. await mock.openChatBoxFor(_converse, contact_jid);
  495. const view = _converse.chatboxviews.get(contact_jid);
  496. view.model.minimize();
  497. expect(view.model.get('chat_state')).toBe('inactive');
  498. spyOn(_converse.connection, 'send');
  499. view.model.maximize();
  500. await u.waitUntil(() => view.model.get('chat_state') === 'active', 1000);
  501. expect(_converse.connection.send).toHaveBeenCalled();
  502. const calls = _.filter(_converse.connection.send.calls.all(), function (call) {
  503. return call.args[0] instanceof Strophe.Builder;
  504. });
  505. expect(calls.length).toBe(1);
  506. const stanza = calls[0].args[0].tree();
  507. expect(stanza.getAttribute('to')).toBe(contact_jid);
  508. expect(stanza.childNodes.length).toBe(3);
  509. expect(stanza.childNodes[0].tagName).toBe('active');
  510. expect(stanza.childNodes[1].tagName).toBe('no-store');
  511. expect(stanza.childNodes[2].tagName).toBe('no-permanent-store');
  512. done();
  513. }));
  514. });
  515. describe("A composing notification", function () {
  516. it("is sent as soon as the user starts typing a message which is not a command",
  517. mock.initConverse(
  518. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  519. async function (done, _converse) {
  520. await mock.waitForRoster(_converse, 'current');
  521. await mock.openControlBox(_converse);
  522. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  523. await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
  524. await mock.openChatBoxFor(_converse, contact_jid);
  525. var view = _converse.chatboxviews.get(contact_jid);
  526. expect(view.model.get('chat_state')).toBe('active');
  527. spyOn(_converse.connection, 'send');
  528. spyOn(_converse.api, "trigger").and.callThrough();
  529. view.onKeyDown({
  530. target: view.el.querySelector('textarea.chat-textarea'),
  531. keyCode: 1
  532. });
  533. expect(view.model.get('chat_state')).toBe('composing');
  534. expect(_converse.connection.send).toHaveBeenCalled();
  535. const stanza = _converse.connection.send.calls.argsFor(0)[0].tree();
  536. expect(stanza.getAttribute('to')).toBe(contact_jid);
  537. expect(stanza.childNodes.length).toBe(3);
  538. expect(stanza.childNodes[0].tagName).toBe('composing');
  539. expect(stanza.childNodes[1].tagName).toBe('no-store');
  540. expect(stanza.childNodes[2].tagName).toBe('no-permanent-store');
  541. // The notification is not sent again
  542. view.onKeyDown({
  543. target: view.el.querySelector('textarea.chat-textarea'),
  544. keyCode: 1
  545. });
  546. expect(view.model.get('chat_state')).toBe('composing');
  547. expect(_converse.api.trigger.calls.count(), 1);
  548. done();
  549. }));
  550. it("is NOT sent out if send_chat_state_notifications doesn't allow it",
  551. mock.initConverse(
  552. ['rosterGroupsFetched', 'chatBoxesFetched'], {'send_chat_state_notifications': []},
  553. async function (done, _converse) {
  554. await mock.waitForRoster(_converse, 'current');
  555. await mock.openControlBox(_converse);
  556. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  557. await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
  558. await mock.openChatBoxFor(_converse, contact_jid);
  559. var view = _converse.chatboxviews.get(contact_jid);
  560. expect(view.model.get('chat_state')).toBe('active');
  561. spyOn(_converse.connection, 'send');
  562. spyOn(_converse.api, "trigger").and.callThrough();
  563. view.onKeyDown({
  564. target: view.el.querySelector('textarea.chat-textarea'),
  565. keyCode: 1
  566. });
  567. expect(view.model.get('chat_state')).toBe('composing');
  568. expect(_converse.connection.send).not.toHaveBeenCalled();
  569. done();
  570. }));
  571. it("will be shown if received",
  572. mock.initConverse(
  573. ['rosterGroupsFetched'], {},
  574. async function (done, _converse) {
  575. await mock.waitForRoster(_converse, 'current');
  576. await mock.openControlBox(_converse);
  577. // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
  578. const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  579. await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
  580. await mock.openChatBoxFor(_converse, sender_jid);
  581. // <composing> state
  582. let msg = $msg({
  583. from: sender_jid,
  584. to: _converse.connection.jid,
  585. type: 'chat',
  586. id: u.getUniqueId()
  587. }).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
  588. _converse.connection._dataRecv(mock.createRequest(msg));
  589. const view = _converse.chatboxviews.get(sender_jid);
  590. let csn = mock.cur_names[1] + ' is typing';
  591. await u.waitUntil( () => view.el.querySelector('.chat-content__notifications').innerText === csn);
  592. expect(view.model.messages.length).toEqual(0);
  593. // <paused> state
  594. msg = $msg({
  595. from: sender_jid,
  596. to: _converse.connection.jid,
  597. type: 'chat',
  598. id: u.getUniqueId()
  599. }).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
  600. _converse.connection._dataRecv(mock.createRequest(msg));
  601. csn = mock.cur_names[1] + ' has stopped typing';
  602. await u.waitUntil( () => view.el.querySelector('.chat-content__notifications').innerText === csn);
  603. msg = $msg({
  604. from: sender_jid,
  605. to: _converse.connection.jid,
  606. type: 'chat',
  607. id: u.getUniqueId()
  608. }).c('body').t('hello world').tree();
  609. await _converse.handleMessageStanza(msg);
  610. const msg_el = await u.waitUntil(() => view.content.querySelector('.chat-msg'));
  611. await u.waitUntil( () => view.el.querySelector('.chat-content__notifications').innerText === '');
  612. expect(msg_el.querySelector('.chat-msg__text').textContent).toBe('hello world');
  613. done();
  614. }));
  615. it("is ignored if it's a composing carbon message sent by this user from a different client",
  616. mock.initConverse(
  617. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  618. async function (done, _converse) {
  619. await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
  620. await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
  621. await mock.waitForRoster(_converse, 'current');
  622. // Send a message from a different resource
  623. const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  624. const view = await mock.openChatBoxFor(_converse, recipient_jid);
  625. spyOn(u, 'shouldCreateMessage').and.callThrough();
  626. const msg = $msg({
  627. 'from': _converse.bare_jid,
  628. 'id': u.getUniqueId(),
  629. 'to': _converse.connection.jid,
  630. 'type': 'chat',
  631. 'xmlns': 'jabber:client'
  632. }).c('sent', {'xmlns': 'urn:xmpp:carbons:2'})
  633. .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
  634. .c('message', {
  635. 'xmlns': 'jabber:client',
  636. 'from': _converse.bare_jid+'/another-resource',
  637. 'to': recipient_jid,
  638. 'type': 'chat'
  639. }).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
  640. _converse.connection._dataRecv(mock.createRequest(msg));
  641. await u.waitUntil(() => u.shouldCreateMessage.calls.count());
  642. expect(view.model.messages.length).toEqual(0);
  643. const el = view.el.querySelector('.chat-content__notifications');
  644. expect(el.textContent).toBe('');
  645. done();
  646. }));
  647. });
  648. describe("A paused notification", function () {
  649. it("is sent if the user has stopped typing since 30 seconds",
  650. mock.initConverse(
  651. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  652. async function (done, _converse) {
  653. await mock.waitForRoster(_converse, 'current');
  654. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  655. await mock.openControlBox(_converse);
  656. await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length, 700);
  657. _converse.TIMEOUTS.PAUSED = 200; // Make the timeout shorter so that we can test
  658. await mock.openChatBoxFor(_converse, contact_jid);
  659. const view = _converse.chatboxviews.get(contact_jid);
  660. spyOn(_converse.connection, 'send');
  661. spyOn(view.model, 'setChatState').and.callThrough();
  662. expect(view.model.get('chat_state')).toBe('active');
  663. view.onKeyDown({
  664. target: view.el.querySelector('textarea.chat-textarea'),
  665. keyCode: 1
  666. });
  667. expect(view.model.get('chat_state')).toBe('composing');
  668. expect(_converse.connection.send).toHaveBeenCalled();
  669. let stanza = _converse.connection.send.calls.argsFor(0)[0].tree();
  670. expect(stanza.childNodes[0].tagName).toBe('composing');
  671. await u.waitUntil(() => view.model.get('chat_state') === 'paused', 500);
  672. expect(_converse.connection.send).toHaveBeenCalled();
  673. var calls = _.filter(_converse.connection.send.calls.all(), function (call) {
  674. return call.args[0] instanceof Strophe.Builder;
  675. });
  676. expect(calls.length).toBe(2);
  677. stanza = calls[1].args[0].tree();
  678. expect(stanza.getAttribute('to')).toBe(contact_jid);
  679. expect(stanza.childNodes.length).toBe(3);
  680. expect(stanza.childNodes[0].tagName).toBe('paused');
  681. expect(stanza.childNodes[1].tagName).toBe('no-store');
  682. expect(stanza.childNodes[2].tagName).toBe('no-permanent-store');
  683. // Test #359. A paused notification should not be sent
  684. // out if the user simply types longer than the
  685. // timeout.
  686. view.onKeyDown({
  687. target: view.el.querySelector('textarea.chat-textarea'),
  688. keyCode: 1
  689. });
  690. expect(view.model.setChatState).toHaveBeenCalled();
  691. expect(view.model.get('chat_state')).toBe('composing');
  692. view.onKeyDown({
  693. target: view.el.querySelector('textarea.chat-textarea'),
  694. keyCode: 1
  695. });
  696. expect(view.model.get('chat_state')).toBe('composing');
  697. done();
  698. }));
  699. it("will be shown if received",
  700. mock.initConverse(
  701. ['rosterGroupsFetched'], {},
  702. async function (done, _converse) {
  703. await mock.waitForRoster(_converse, 'current');
  704. await mock.openControlBox(_converse);
  705. await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
  706. // TODO: only show paused state if the previous state was composing
  707. // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
  708. spyOn(_converse.api, "trigger").and.callThrough();
  709. const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  710. const view = await mock.openChatBoxFor(_converse, sender_jid);
  711. // <paused> state
  712. const msg = $msg({
  713. from: sender_jid,
  714. to: _converse.connection.jid,
  715. type: 'chat',
  716. id: u.getUniqueId()
  717. }).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
  718. _converse.connection._dataRecv(mock.createRequest(msg));
  719. const csn = mock.cur_names[1] + ' has stopped typing';
  720. await u.waitUntil( () => view.el.querySelector('.chat-content__notifications').innerText === csn);
  721. expect(view.model.messages.length).toEqual(0);
  722. done();
  723. }));
  724. it("will not be shown if it's a paused carbon message that this user sent from a different client",
  725. mock.initConverse(
  726. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  727. async function (done, _converse) {
  728. await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
  729. await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
  730. await mock.waitForRoster(_converse, 'current');
  731. // Send a message from a different resource
  732. const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  733. spyOn(u, 'shouldCreateMessage').and.callThrough();
  734. const view = await mock.openChatBoxFor(_converse, recipient_jid);
  735. const msg = $msg({
  736. 'from': _converse.bare_jid,
  737. 'id': u.getUniqueId(),
  738. 'to': _converse.connection.jid,
  739. 'type': 'chat',
  740. 'xmlns': 'jabber:client'
  741. }).c('sent', {'xmlns': 'urn:xmpp:carbons:2'})
  742. .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
  743. .c('message', {
  744. 'xmlns': 'jabber:client',
  745. 'from': _converse.bare_jid+'/another-resource',
  746. 'to': recipient_jid,
  747. 'type': 'chat'
  748. }).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
  749. _converse.connection._dataRecv(mock.createRequest(msg));
  750. await u.waitUntil(() => u.shouldCreateMessage.calls.count());
  751. expect(view.model.messages.length).toEqual(0);
  752. const el = view.el.querySelector('.chat-content__notifications');
  753. expect(el.textContent).toBe('');
  754. done();
  755. done();
  756. }));
  757. });
  758. describe("An inactive notifciation", function () {
  759. it("is sent if the user has stopped typing since 2 minutes",
  760. mock.initConverse(
  761. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  762. async function (done, _converse) {
  763. const sent_stanzas = _converse.connection.sent_stanzas;
  764. // Make the timeouts shorter so that we can test
  765. _converse.TIMEOUTS.PAUSED = 100;
  766. _converse.TIMEOUTS.INACTIVE = 100;
  767. await mock.waitForRoster(_converse, 'current');
  768. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  769. await mock.openControlBox(_converse);
  770. await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 1000);
  771. await mock.openChatBoxFor(_converse, contact_jid);
  772. const view = _converse.chatboxviews.get(contact_jid);
  773. await u.waitUntil(() => view.model.get('chat_state') === 'active');
  774. let messages = await u.waitUntil(() => sent_stanzas.filter(s => s.matches('message')));
  775. expect(messages.length).toBe(1);
  776. expect(view.model.get('chat_state')).toBe('active');
  777. view.onKeyDown({
  778. target: view.el.querySelector('textarea.chat-textarea'),
  779. keyCode: 1
  780. });
  781. await u.waitUntil(() => view.model.get('chat_state') === 'composing', 600);
  782. messages = sent_stanzas.filter(s => s.matches('message'));
  783. expect(messages.length).toBe(2);
  784. await u.waitUntil(() => view.model.get('chat_state') === 'paused', 600);
  785. messages = sent_stanzas.filter(s => s.matches('message'));
  786. expect(messages.length).toBe(3);
  787. await u.waitUntil(() => view.model.get('chat_state') === 'inactive', 600);
  788. messages = sent_stanzas.filter(s => s.matches('message'));
  789. expect(messages.length).toBe(4);
  790. expect(Strophe.serialize(messages[0])).toBe(
  791. `<message id="${messages[0].getAttribute('id')}" to="mercutio@montague.lit" type="chat" xmlns="jabber:client">`+
  792. `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
  793. `<no-store xmlns="urn:xmpp:hints"/>`+
  794. `<no-permanent-store xmlns="urn:xmpp:hints"/>`+
  795. `</message>`);
  796. expect(Strophe.serialize(messages[1])).toBe(
  797. `<message id="${messages[1].getAttribute('id')}" to="mercutio@montague.lit" type="chat" xmlns="jabber:client">`+
  798. `<composing xmlns="http://jabber.org/protocol/chatstates"/>`+
  799. `<no-store xmlns="urn:xmpp:hints"/>`+
  800. `<no-permanent-store xmlns="urn:xmpp:hints"/>`+
  801. `</message>`);
  802. expect(Strophe.serialize(messages[2])).toBe(
  803. `<message id="${messages[2].getAttribute('id')}" to="mercutio@montague.lit" type="chat" xmlns="jabber:client">`+
  804. `<paused xmlns="http://jabber.org/protocol/chatstates"/>`+
  805. `<no-store xmlns="urn:xmpp:hints"/>`+
  806. `<no-permanent-store xmlns="urn:xmpp:hints"/>`+
  807. `</message>`);
  808. expect(Strophe.serialize(messages[3])).toBe(
  809. `<message id="${messages[3].getAttribute('id')}" to="mercutio@montague.lit" type="chat" xmlns="jabber:client">`+
  810. `<inactive xmlns="http://jabber.org/protocol/chatstates"/>`+
  811. `<no-store xmlns="urn:xmpp:hints"/>`+
  812. `<no-permanent-store xmlns="urn:xmpp:hints"/>`+
  813. `</message>`);
  814. done();
  815. }));
  816. it("is sent when the user a minimizes a chat box",
  817. mock.initConverse(
  818. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  819. async function (done, _converse) {
  820. await mock.waitForRoster(_converse, 'current');
  821. await mock.openControlBox(_converse);
  822. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  823. await mock.openChatBoxFor(_converse, contact_jid);
  824. const view = _converse.chatboxviews.get(contact_jid);
  825. spyOn(_converse.connection, 'send');
  826. view.minimize();
  827. expect(view.model.get('chat_state')).toBe('inactive');
  828. expect(_converse.connection.send).toHaveBeenCalled();
  829. var stanza = _converse.connection.send.calls.argsFor(0)[0].tree();
  830. expect(stanza.getAttribute('to')).toBe(contact_jid);
  831. expect(stanza.childNodes[0].tagName).toBe('inactive');
  832. done();
  833. }));
  834. it("is sent if the user closes a chat box",
  835. mock.initConverse(
  836. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  837. async function (done, _converse) {
  838. await mock.waitForRoster(_converse, 'current');
  839. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  840. await mock.openControlBox(_converse);
  841. await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
  842. const view = await mock.openChatBoxFor(_converse, contact_jid);
  843. expect(view.model.get('chat_state')).toBe('active');
  844. spyOn(_converse.connection, 'send');
  845. view.close();
  846. expect(view.model.get('chat_state')).toBe('inactive');
  847. expect(_converse.connection.send).toHaveBeenCalled();
  848. const stanza = _converse.connection.send.calls.argsFor(0)[0].tree();
  849. expect(stanza.getAttribute('to')).toBe(contact_jid);
  850. expect(stanza.childNodes.length).toBe(3);
  851. expect(stanza.childNodes[0].tagName).toBe('inactive');
  852. expect(stanza.childNodes[1].tagName).toBe('no-store');
  853. expect(stanza.childNodes[2].tagName).toBe('no-permanent-store');
  854. done();
  855. }));
  856. it("will clear any other chat status notifications",
  857. mock.initConverse(
  858. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  859. async function (done, _converse) {
  860. await mock.waitForRoster(_converse, 'current');
  861. await mock.openControlBox(_converse);
  862. const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  863. // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
  864. await mock.openChatBoxFor(_converse, sender_jid);
  865. const view = _converse.chatboxviews.get(sender_jid);
  866. expect(view.el.querySelectorAll('.chat-event').length).toBe(0);
  867. // Insert <composing> message, to also check that
  868. // text messages are inserted correctly with
  869. // temporary chat events in the chat contents.
  870. let msg = $msg({
  871. 'to': _converse.bare_jid,
  872. 'xmlns': 'jabber:client',
  873. 'from': sender_jid,
  874. 'type': 'chat'})
  875. .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
  876. .tree();
  877. _converse.connection._dataRecv(mock.createRequest(msg));
  878. const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent);
  879. expect(csntext).toEqual(mock.cur_names[1] + ' is typing');
  880. expect(view.model.messages.length).toBe(0);
  881. msg = $msg({
  882. from: sender_jid,
  883. to: _converse.connection.jid,
  884. type: 'chat',
  885. id: u.getUniqueId()
  886. }).c('inactive', {'xmlns': Strophe.NS.CHATSTATES}).tree();
  887. _converse.connection._dataRecv(mock.createRequest(msg));
  888. await u.waitUntil(() => !view.el.querySelector('.chat-content__notifications').textContent);
  889. done();
  890. }));
  891. });
  892. describe("A gone notifciation", function () {
  893. it("will be shown if received",
  894. mock.initConverse(
  895. ['rosterGroupsFetched'], {},
  896. async function (done, _converse) {
  897. await mock.waitForRoster(_converse, 'current', 3);
  898. await mock.openControlBox(_converse);
  899. const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  900. await mock.openChatBoxFor(_converse, sender_jid);
  901. const msg = $msg({
  902. from: sender_jid,
  903. to: _converse.connection.jid,
  904. type: 'chat',
  905. id: u.getUniqueId()
  906. }).c('body').c('gone', {'xmlns': Strophe.NS.CHATSTATES}).tree();
  907. _converse.connection._dataRecv(mock.createRequest(msg));
  908. const view = _converse.chatboxviews.get(sender_jid);
  909. const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent);
  910. expect(csntext).toEqual(mock.cur_names[1] + ' has gone away');
  911. done();
  912. }));
  913. });
  914. describe("On receiving a message correction", function () {
  915. it("will be removed",
  916. mock.initConverse(
  917. ['rosterGroupsFetched'], {},
  918. async function (done, _converse) {
  919. await mock.waitForRoster(_converse, 'current');
  920. await mock.openControlBox(_converse);
  921. // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
  922. const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  923. await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
  924. await mock.openChatBoxFor(_converse, sender_jid);
  925. // Original message
  926. const original_id = u.getUniqueId();
  927. const original = $msg({
  928. from: sender_jid,
  929. to: _converse.connection.jid,
  930. type: 'chat',
  931. id: original_id,
  932. body: "Original message",
  933. }).c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree();
  934. spyOn(_converse.api, "trigger").and.callThrough();
  935. _converse.connection._dataRecv(mock.createRequest(original));
  936. await u.waitUntil(() => _converse.api.trigger.calls.count());
  937. expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
  938. const view = _converse.chatboxviews.get(sender_jid);
  939. expect(view).toBeDefined();
  940. // <composing> state
  941. const msg = $msg({
  942. from: sender_jid,
  943. to: _converse.connection.jid,
  944. type: 'chat',
  945. id: u.getUniqueId()
  946. }).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
  947. _converse.connection._dataRecv(mock.createRequest(msg));
  948. const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent);
  949. expect(csntext).toEqual(mock.cur_names[1] + ' is typing');
  950. // Edited message
  951. const edited = $msg({
  952. from: sender_jid,
  953. to: _converse.connection.jid,
  954. type: 'chat',
  955. id: u.getUniqueId(),
  956. body: "Edited message",
  957. })
  958. .c('active', {'xmlns': Strophe.NS.CHATSTATES}).up()
  959. .c('replace', {'xmlns': Strophe.NS.MESSAGE_CORRECT, 'id': original_id }).tree();
  960. await _converse.handleMessageStanza(edited);
  961. await u.waitUntil(() => !view.el.querySelector('.chat-content__notifications').textContent);
  962. done();
  963. }));
  964. });
  965. });
  966. });
  967. describe("Special Messages", function () {
  968. it("'/clear' can be used to clear messages in a conversation",
  969. mock.initConverse(
  970. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  971. async function (done, _converse) {
  972. await mock.waitForRoster(_converse, 'current');
  973. await mock.openControlBox(_converse);
  974. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  975. spyOn(_converse.api, "trigger").and.callThrough();
  976. await mock.openChatBoxFor(_converse, contact_jid);
  977. const view = _converse.chatboxviews.get(contact_jid);
  978. let message = 'This message is another sent from this chatbox';
  979. await mock.sendMessage(view, message);
  980. expect(view.model.messages.length === 1).toBeTruthy();
  981. let stored_messages = await view.model.messages.browserStorage.findAll();
  982. expect(stored_messages.length).toBe(1);
  983. await u.waitUntil(() => view.el.querySelector('.chat-msg'));
  984. message = '/clear';
  985. spyOn(view, 'clearMessages').and.callThrough();
  986. spyOn(window, 'confirm').and.callFake(function () {
  987. return true;
  988. });
  989. view.el.querySelector('.chat-textarea').value = message;
  990. view.onKeyDown({
  991. target: view.el.querySelector('textarea.chat-textarea'),
  992. preventDefault: function preventDefault () {},
  993. keyCode: 13
  994. });
  995. expect(view.clearMessages.calls.all().length).toBe(1);
  996. await view.clearMessages.calls.all()[0].returnValue;
  997. expect(window.confirm).toHaveBeenCalled();
  998. expect(view.model.messages.length, 0); // The messages must be removed from the chatbox
  999. stored_messages = await view.model.messages.browserStorage.findAll();
  1000. expect(stored_messages.length).toBe(0);
  1001. expect(_converse.api.trigger.calls.count(), 1);
  1002. expect(_converse.api.trigger.calls.mostRecent().args, ['messageSend', message]);
  1003. done();
  1004. }));
  1005. });
  1006. describe("A Message Counter", function () {
  1007. it("is incremented when the message is received and the window is not focused",
  1008. mock.initConverse(
  1009. ['rosterGroupsFetched'], {},
  1010. async function (done, _converse) {
  1011. await mock.waitForRoster(_converse, 'current');
  1012. await mock.openControlBox(_converse);
  1013. expect(document.title).toBe('Converse Tests');
  1014. const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  1015. const view = await mock.openChatBoxFor(_converse, sender_jid)
  1016. const previous_state = _converse.windowState;
  1017. const message = 'This message will increment the message counter';
  1018. const msg = $msg({
  1019. from: sender_jid,
  1020. to: _converse.connection.jid,
  1021. type: 'chat',
  1022. id: u.getUniqueId()
  1023. }).c('body').t(message).up()
  1024. .c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree();
  1025. _converse.windowState = 'hidden';
  1026. spyOn(_converse.api, "trigger").and.callThrough();
  1027. spyOn(_converse, 'incrementMsgCounter').and.callThrough();
  1028. spyOn(_converse, 'clearMsgCounter').and.callThrough();
  1029. await _converse.handleMessageStanza(msg);
  1030. await new Promise(resolve => view.once('messageInserted', resolve));
  1031. expect(_converse.incrementMsgCounter).toHaveBeenCalled();
  1032. expect(_converse.clearMsgCounter).not.toHaveBeenCalled();
  1033. expect(document.title).toBe('Messages (1) Converse Tests');
  1034. expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
  1035. _converse.windowSate = previous_state;
  1036. done();
  1037. }));
  1038. it("is cleared when the window is focused",
  1039. mock.initConverse(
  1040. ['rosterGroupsFetched'], {},
  1041. async function (done, _converse) {
  1042. await mock.waitForRoster(_converse, 'current');
  1043. await mock.openControlBox(_converse);
  1044. _converse.windowState = 'hidden';
  1045. spyOn(_converse, 'clearMsgCounter').and.callThrough();
  1046. _converse.saveWindowState(null, 'focus');
  1047. _converse.saveWindowState(null, 'blur');
  1048. expect(_converse.clearMsgCounter).toHaveBeenCalled();
  1049. done();
  1050. }));
  1051. it("is not incremented when the message is received and the window is focused",
  1052. mock.initConverse(
  1053. ['rosterGroupsFetched'], {},
  1054. async function (done, _converse) {
  1055. await mock.waitForRoster(_converse, 'current');
  1056. await mock.openControlBox(_converse);
  1057. expect(document.title).toBe('Converse Tests');
  1058. spyOn(_converse, 'incrementMsgCounter').and.callThrough();
  1059. _converse.saveWindowState(null, 'focus');
  1060. const message = 'This message will not increment the message counter';
  1061. const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
  1062. msg = $msg({
  1063. from: sender_jid,
  1064. to: _converse.connection.jid,
  1065. type: 'chat',
  1066. id: u.getUniqueId()
  1067. }).c('body').t(message).up()
  1068. .c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree();
  1069. await _converse.handleMessageStanza(msg);
  1070. expect(_converse.incrementMsgCounter).not.toHaveBeenCalled();
  1071. expect(document.title).toBe('Converse Tests');
  1072. done();
  1073. }));
  1074. it("is incremented from zero when chatbox was closed after viewing previously received messages and the window is not focused now",
  1075. mock.initConverse(
  1076. ['rosterGroupsFetched'], {},
  1077. async function (done, _converse) {
  1078. await mock.waitForRoster(_converse, 'current');
  1079. // initial state
  1080. expect(document.title).toBe('Converse Tests');
  1081. const message = 'This message will always increment the message counter from zero',
  1082. sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
  1083. msgFactory = function () {
  1084. return $msg({
  1085. from: sender_jid,
  1086. to: _converse.connection.jid,
  1087. type: 'chat',
  1088. id: u.getUniqueId()
  1089. })
  1090. .c('body').t(message).up()
  1091. .c('active', {'xmlns': Strophe.NS.CHATSTATES})
  1092. .tree();
  1093. };
  1094. // leave converse-chat page
  1095. _converse.windowState = 'hidden';
  1096. await _converse.handleMessageStanza(msgFactory());
  1097. let view = _converse.chatboxviews.get(sender_jid);
  1098. expect(document.title).toBe('Messages (1) Converse Tests');
  1099. // come back to converse-chat page
  1100. _converse.saveWindowState(null, 'focus');
  1101. await u.waitUntil(() => u.isVisible(view.el));
  1102. expect(document.title).toBe('Converse Tests');
  1103. // close chatbox and leave converse-chat page again
  1104. view.close();
  1105. _converse.windowState = 'hidden';
  1106. // check that msg_counter is incremented from zero again
  1107. await _converse.handleMessageStanza(msgFactory());
  1108. view = _converse.chatboxviews.get(sender_jid);
  1109. await u.waitUntil(() => u.isVisible(view.el));
  1110. expect(document.title).toBe('Messages (1) Converse Tests');
  1111. done();
  1112. }));
  1113. });
  1114. describe("A ChatBox's Unread Message Count", function () {
  1115. it("is incremented when the message is received and ChatBoxView is scrolled up",
  1116. mock.initConverse(
  1117. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  1118. async function (done, _converse) {
  1119. await mock.waitForRoster(_converse, 'current');
  1120. const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
  1121. msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
  1122. const view = await mock.openChatBoxFor(_converse, sender_jid)
  1123. view.model.save('scrolled', true);
  1124. await _converse.handleMessageStanza(msg);
  1125. await u.waitUntil(() => view.model.messages.length);
  1126. expect(view.model.get('num_unread')).toBe(1);
  1127. const msgid = view.model.messages.last().get('id');
  1128. expect(view.model.get('first_unread_id')).toBe(msgid);
  1129. done();
  1130. }));
  1131. it("is not incremented when the message is received and ChatBoxView is scrolled down",
  1132. mock.initConverse(
  1133. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  1134. async function (done, _converse) {
  1135. await mock.waitForRoster(_converse, 'current');
  1136. const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
  1137. msg = mock.createChatMessage(_converse, sender_jid, 'This message will be read');
  1138. await mock.openChatBoxFor(_converse, sender_jid);
  1139. const chatbox = _converse.chatboxes.get(sender_jid);
  1140. await _converse.handleMessageStanza(msg);
  1141. expect(chatbox.get('num_unread')).toBe(0);
  1142. done();
  1143. }));
  1144. it("is incremeted when message is received, chatbox is scrolled down and the window is not focused",
  1145. mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
  1146. async function (done, _converse) {
  1147. await mock.waitForRoster(_converse, 'current');
  1148. const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  1149. const msgFactory = function () {
  1150. return mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
  1151. };
  1152. await mock.openChatBoxFor(_converse, sender_jid);
  1153. const chatbox = _converse.chatboxes.get(sender_jid);
  1154. _converse.windowState = 'hidden';
  1155. const msg = msgFactory();
  1156. _converse.handleMessageStanza(msg);
  1157. await u.waitUntil(() => chatbox.messages.length);
  1158. expect(chatbox.get('num_unread')).toBe(1);
  1159. const msgid = chatbox.messages.last().get('id');
  1160. expect(chatbox.get('first_unread_id')).toBe(msgid);
  1161. done();
  1162. }));
  1163. it("is incremeted when message is received, chatbox is scrolled up and the window is not focused",
  1164. mock.initConverse(
  1165. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  1166. async function (done, _converse) {
  1167. await mock.waitForRoster(_converse, 'current', 1);
  1168. const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  1169. const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
  1170. await mock.openChatBoxFor(_converse, sender_jid);
  1171. const chatbox = _converse.chatboxes.get(sender_jid);
  1172. chatbox.save('scrolled', true);
  1173. _converse.windowState = 'hidden';
  1174. const msg = msgFactory();
  1175. _converse.handleMessageStanza(msg);
  1176. await u.waitUntil(() => chatbox.messages.length);
  1177. expect(chatbox.get('num_unread')).toBe(1);
  1178. const msgid = chatbox.messages.last().get('id');
  1179. expect(chatbox.get('first_unread_id')).toBe(msgid);
  1180. done();
  1181. }));
  1182. it("is cleared when ChatBoxView was scrolled down and the window become focused",
  1183. mock.initConverse(
  1184. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  1185. async function (done, _converse) {
  1186. await mock.waitForRoster(_converse, 'current', 1);
  1187. const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  1188. const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
  1189. await mock.openChatBoxFor(_converse, sender_jid);
  1190. const chatbox = _converse.chatboxes.get(sender_jid);
  1191. _converse.windowState = 'hidden';
  1192. const msg = msgFactory();
  1193. _converse.handleMessageStanza(msg);
  1194. await u.waitUntil(() => chatbox.messages.length);
  1195. expect(chatbox.get('num_unread')).toBe(1);
  1196. const msgid = chatbox.messages.last().get('id');
  1197. expect(chatbox.get('first_unread_id')).toBe(msgid);
  1198. _converse.saveWindowState(null, 'focus');
  1199. expect(chatbox.get('num_unread')).toBe(0);
  1200. done();
  1201. }));
  1202. it("is not cleared when ChatBoxView was scrolled up and the windows become focused",
  1203. mock.initConverse(
  1204. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  1205. async function (done, _converse) {
  1206. await mock.waitForRoster(_converse, 'current', 1);
  1207. const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  1208. const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
  1209. await mock.openChatBoxFor(_converse, sender_jid);
  1210. const chatbox = _converse.chatboxes.get(sender_jid);
  1211. chatbox.save('scrolled', true);
  1212. _converse.windowState = 'hidden';
  1213. const msg = msgFactory();
  1214. _converse.handleMessageStanza(msg);
  1215. await u.waitUntil(() => chatbox.messages.length);
  1216. expect(chatbox.get('num_unread')).toBe(1);
  1217. const msgid = chatbox.messages.last().get('id');
  1218. expect(chatbox.get('first_unread_id')).toBe(msgid);
  1219. _converse.saveWindowState(null, 'focus');
  1220. expect(chatbox.get('num_unread')).toBe(1);
  1221. expect(chatbox.get('first_unread_id')).toBe(msgid);
  1222. done();
  1223. }));
  1224. });
  1225. describe("A RosterView's Unread Message Count", function () {
  1226. it("is updated when message is received and chatbox is scrolled up",
  1227. mock.initConverse(
  1228. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  1229. async function (done, _converse) {
  1230. await mock.waitForRoster(_converse, 'current', 1);
  1231. let msg, indicator_el;
  1232. const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  1233. await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 500);
  1234. await mock.openChatBoxFor(_converse, sender_jid);
  1235. const chatbox = _converse.chatboxes.get(sender_jid);
  1236. chatbox.save('scrolled', true);
  1237. msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
  1238. await _converse.handleMessageStanza(msg);
  1239. await u.waitUntil(() => chatbox.messages.length);
  1240. const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
  1241. indicator_el = sizzle(selector, _converse.rosterview.el).pop();
  1242. expect(indicator_el.textContent).toBe('1');
  1243. msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread too');
  1244. await _converse.handleMessageStanza(msg);
  1245. await u.waitUntil(() => chatbox.messages.length > 1);
  1246. indicator_el = sizzle(selector, _converse.rosterview.el).pop();
  1247. expect(indicator_el.textContent).toBe('2');
  1248. done();
  1249. }));
  1250. it("is updated when message is received and chatbox is minimized",
  1251. mock.initConverse(
  1252. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  1253. async function (done, _converse) {
  1254. await mock.waitForRoster(_converse, 'current', 1);
  1255. const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  1256. let indicator_el, msg;
  1257. await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 500);
  1258. await mock.openChatBoxFor(_converse, sender_jid);
  1259. const chatbox = _converse.chatboxes.get(sender_jid);
  1260. var chatboxview = _converse.chatboxviews.get(sender_jid);
  1261. chatboxview.minimize();
  1262. msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
  1263. await _converse.handleMessageStanza(msg);
  1264. await u.waitUntil(() => chatbox.messages.length);
  1265. const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
  1266. indicator_el = sizzle(selector, _converse.rosterview.el).pop();
  1267. expect(indicator_el.textContent).toBe('1');
  1268. msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread too');
  1269. await _converse.handleMessageStanza(msg);
  1270. await u.waitUntil(() => chatbox.messages.length === 2);
  1271. indicator_el = sizzle(selector, _converse.rosterview.el).pop();
  1272. expect(indicator_el.textContent).toBe('2');
  1273. done();
  1274. }));
  1275. it("is cleared when chatbox is maximzied after receiving messages in minimized mode",
  1276. mock.initConverse(
  1277. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  1278. async function (done, _converse) {
  1279. await mock.waitForRoster(_converse, 'current', 1);
  1280. const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  1281. const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
  1282. await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 500);
  1283. await mock.openChatBoxFor(_converse, sender_jid);
  1284. const chatbox = _converse.chatboxes.get(sender_jid);
  1285. const view = _converse.chatboxviews.get(sender_jid);
  1286. const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
  1287. const select_msgs_indicator = () => sizzle(selector, _converse.rosterview.el).pop();
  1288. view.minimize();
  1289. _converse.handleMessageStanza(msgFactory());
  1290. await u.waitUntil(() => chatbox.messages.length);
  1291. expect(select_msgs_indicator().textContent).toBe('1');
  1292. _converse.handleMessageStanza(msgFactory());
  1293. await u.waitUntil(() => chatbox.messages.length > 1);
  1294. expect(select_msgs_indicator().textContent).toBe('2');
  1295. view.model.maximize();
  1296. u.waitUntil(() => typeof select_msgs_indicator() === 'undefined');
  1297. done();
  1298. }));
  1299. it("is cleared when unread messages are viewed which were received in scrolled-up chatbox",
  1300. mock.initConverse(
  1301. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  1302. async function (done, _converse) {
  1303. await mock.openControlBox(_converse);
  1304. await mock.waitForRoster(_converse, 'current', 1);
  1305. const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  1306. await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 500);
  1307. await mock.openChatBoxFor(_converse, sender_jid);
  1308. const chatbox = _converse.chatboxes.get(sender_jid);
  1309. const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
  1310. const selector = `a.open-chat:contains("${chatbox.get('nickname')}") .msgs-indicator`;
  1311. const select_msgs_indicator = () => sizzle(selector, _converse.rosterview.el).pop();
  1312. chatbox.save('scrolled', true);
  1313. _converse.handleMessageStanza(msgFactory());
  1314. const view = _converse.chatboxviews.get(sender_jid);
  1315. await u.waitUntil(() => view.model.messages.length);
  1316. expect(select_msgs_indicator().textContent).toBe('1');
  1317. view.viewUnreadMessages();
  1318. _converse.rosterview.render();
  1319. expect(select_msgs_indicator()).toBeUndefined();
  1320. done();
  1321. }));
  1322. it("is not cleared after user clicks on roster view when chatbox is already opened and scrolled up",
  1323. mock.initConverse(
  1324. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  1325. async function (done, _converse) {
  1326. await mock.waitForRoster(_converse, 'current', 1);
  1327. const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  1328. await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 500);
  1329. await mock.openChatBoxFor(_converse, sender_jid);
  1330. const chatbox = _converse.chatboxes.get(sender_jid);
  1331. const view = _converse.chatboxviews.get(sender_jid);
  1332. const msg = 'This message will be received as unread, but eventually will be read';
  1333. const msgFactory = () => mock.createChatMessage(_converse, sender_jid, msg);
  1334. const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
  1335. const select_msgs_indicator = () => sizzle(selector, _converse.rosterview.el).pop();
  1336. chatbox.save('scrolled', true);
  1337. _converse.handleMessageStanza(msgFactory());
  1338. await u.waitUntil(() => view.model.messages.length);
  1339. expect(select_msgs_indicator().textContent).toBe('1');
  1340. await mock.openChatBoxFor(_converse, sender_jid);
  1341. expect(select_msgs_indicator().textContent).toBe('1');
  1342. done();
  1343. }));
  1344. });
  1345. describe("A Minimized ChatBoxView's Unread Message Count", function () {
  1346. it("is displayed when scrolled up chatbox is minimized after receiving unread messages",
  1347. mock.initConverse(
  1348. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  1349. async function (done, _converse) {
  1350. await mock.waitForRoster(_converse, 'current', 1);
  1351. const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  1352. await mock.openChatBoxFor(_converse, sender_jid);
  1353. const msgFactory = function () {
  1354. return mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
  1355. };
  1356. const selectUnreadMsgCount = function () {
  1357. const minimizedChatBoxView = _converse.minimized_chats.get(sender_jid);
  1358. return minimizedChatBoxView.el.querySelector('.message-count');
  1359. };
  1360. const chatbox = _converse.chatboxes.get(sender_jid);
  1361. chatbox.save('scrolled', true);
  1362. _converse.handleMessageStanza(msgFactory());
  1363. await u.waitUntil(() => chatbox.messages.length);
  1364. const chatboxview = _converse.chatboxviews.get(sender_jid);
  1365. chatboxview.minimize();
  1366. const unread_count = selectUnreadMsgCount();
  1367. expect(u.isVisible(unread_count)).toBeTruthy();
  1368. expect(unread_count.innerHTML).toBe('1');
  1369. done();
  1370. }));
  1371. it("is incremented when message is received and windows is not focused",
  1372. mock.initConverse(
  1373. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  1374. async function (done, _converse) {
  1375. await mock.waitForRoster(_converse, 'current', 1);
  1376. const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  1377. const view = await mock.openChatBoxFor(_converse, sender_jid)
  1378. const msgFactory = function () {
  1379. return mock.createChatMessage(_converse, sender_jid,
  1380. 'This message will be received as unread, but eventually will be read');
  1381. };
  1382. const selectUnreadMsgCount = function () {
  1383. const minimizedChatBoxView = _converse.minimized_chats.get(sender_jid);
  1384. return minimizedChatBoxView.el.querySelector('.message-count');
  1385. };
  1386. view.minimize();
  1387. _converse.handleMessageStanza(msgFactory());
  1388. await u.waitUntil(() => view.model.messages.length);
  1389. const unread_count = selectUnreadMsgCount();
  1390. expect(u.isVisible(unread_count)).toBeTruthy();
  1391. expect(unread_count.innerHTML).toBe('1');
  1392. done();
  1393. }));
  1394. it("will render Openstreetmap-URL from geo-URI",
  1395. mock.initConverse(
  1396. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  1397. async function (done, _converse) {
  1398. await mock.waitForRoster(_converse, 'current', 1);
  1399. const message = "geo:37.786971,-122.399677",
  1400. contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  1401. await mock.openChatBoxFor(_converse, contact_jid);
  1402. const view = _converse.chatboxviews.get(contact_jid);
  1403. spyOn(view.model, 'sendMessage').and.callThrough();
  1404. mock.sendMessage(view, message);
  1405. await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg').length, 1000);
  1406. expect(view.model.sendMessage).toHaveBeenCalled();
  1407. const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
  1408. expect(msg.innerHTML).toEqual(
  1409. '<a target="_blank" rel="noopener" href="https://www.openstreetmap.org/?mlat=37.786971&amp;'+
  1410. 'mlon=-122.399677#map=18/37.786971/-122.399677">https://www.openstreetmap.org/?mlat=37.7869'+
  1411. '71&amp;mlon=-122.399677#map=18/37.786971/-122.399677</a>');
  1412. done();
  1413. }));
  1414. });
  1415. });