chatbox.js 91 KB

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