chatbox.js 69 KB

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