chatbox.js 91 KB

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