chatbox.js 90 KB

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