chatbox.js 69 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179
  1. /*global converse */
  2. (function (root, factory) {
  3. define([
  4. "jquery",
  5. "underscore",
  6. "mock",
  7. "test_utils"
  8. ], function ($, _, mock, test_utils) {
  9. return factory($, _, mock, test_utils);
  10. }
  11. );
  12. } (this, function ($, _, mock, test_utils) {
  13. var $msg = converse_api.env.$msg;
  14. var Strophe = converse_api.env.Strophe;
  15. var moment = converse_api.env.moment;
  16. return describe("Chatboxes", $.proxy(function(mock, test_utils) {
  17. describe("A Chatbox", function () {
  18. beforeEach(function () {
  19. runs(function () {
  20. test_utils.closeAllChatBoxes();
  21. test_utils.removeControlBox();
  22. test_utils.clearBrowserStorage();
  23. test_utils.initConverse();
  24. test_utils.createContacts('current');
  25. test_utils.openControlBox();
  26. test_utils.openContactsPanel();
  27. });
  28. });
  29. it("is created when you click on a roster item", function () {
  30. var i, $el, jid, chatboxview;
  31. // openControlBox was called earlier, so the controlbox is
  32. // visible, but no other chat boxes have been created.
  33. expect(this.chatboxes.length).toEqual(1);
  34. spyOn(this.chatboxviews, 'trimChats');
  35. expect($("#conversejs .chatbox").length).toBe(1); // Controlbox is open
  36. var online_contacts = this.rosterview.$el.find('dt.roster-group').siblings('dd.current-xmpp-contact').find('a.open-chat');
  37. for (i=0; i<online_contacts.length; i++) {
  38. $el = $(online_contacts[i]);
  39. jid = $el.text().replace(/ /g,'.').toLowerCase() + '@localhost';
  40. $el.click();
  41. chatboxview = this.chatboxviews.get(jid);
  42. expect(this.chatboxes.length).toEqual(i+2);
  43. expect(this.chatboxviews.trimChats).toHaveBeenCalled();
  44. // Check that new chat boxes are created to the left of the
  45. // controlbox (but to the right of all existing chat boxes)
  46. expect($("#conversejs .chatbox").length).toBe(i+2);
  47. expect($("#conversejs .chatbox")[1].id).toBe(chatboxview.model.get('box_id'));
  48. }
  49. }.bind(converse));
  50. it("can be trimmed to conserve space", function () {
  51. var i, $el, jid, chatbox, chatboxview, trimmedview;
  52. // openControlBox was called earlier, so the controlbox is
  53. // visible, but no other chat boxes have been created.
  54. var trimmed_chatboxes = converse.minimized_chats;
  55. expect(this.chatboxes.length).toEqual(1);
  56. spyOn(this.chatboxviews, 'trimChats');
  57. spyOn(trimmed_chatboxes, 'addChat').andCallThrough();
  58. spyOn(trimmed_chatboxes, 'removeChat').andCallThrough();
  59. expect($("#conversejs .chatbox").length).toBe(1); // Controlbox is open
  60. // Test that they can be trimmed
  61. runs(function () {
  62. converse.rosterview.update(); // XXX: Hack to make sure $roster element is attaced.
  63. }.bind(this));
  64. waits(50);
  65. runs(function () {
  66. // Test that they can be maximized again
  67. var online_contacts = this.rosterview.$el.find('dt.roster-group').siblings('dd.current-xmpp-contact').find('a.open-chat');
  68. for (i=0; i<online_contacts.length; i++) {
  69. $el = $(online_contacts[i]);
  70. jid = $el.text().replace(/ /g,'.').toLowerCase() + '@localhost';
  71. $el.click();
  72. expect(this.chatboxviews.trimChats).toHaveBeenCalled();
  73. chatboxview = this.chatboxviews.get(jid);
  74. spyOn(chatboxview, 'minimize').andCallThrough();
  75. chatboxview.model.set({'minimized': true});
  76. expect(trimmed_chatboxes.addChat).toHaveBeenCalled();
  77. expect(chatboxview.minimize).toHaveBeenCalled();
  78. trimmedview = trimmed_chatboxes.get(jid);
  79. }
  80. var key = this.chatboxviews.keys()[1];
  81. trimmedview = trimmed_chatboxes.get(key);
  82. chatbox = trimmedview.model;
  83. spyOn(chatbox, 'maximize').andCallThrough();
  84. spyOn(trimmedview, 'restore').andCallThrough();
  85. trimmedview.delegateEvents();
  86. trimmedview.$("a.restore-chat").click();
  87. }.bind(this));
  88. waits(250);
  89. runs(function () {
  90. expect(trimmedview.restore).toHaveBeenCalled();
  91. expect(chatbox.maximize).toHaveBeenCalled();
  92. expect(this.chatboxviews.trimChats).toHaveBeenCalled();
  93. }.bind(this));
  94. }.bind(converse));
  95. it("is focused if its already open and you click on its corresponding roster item", function () {
  96. var contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
  97. var $el, jid, chatboxview, chatbox;
  98. // openControlBox was called earlier, so the controlbox is
  99. // visible, but no other chat boxes have been created.
  100. expect(converse.chatboxes.length).toEqual(1);
  101. chatbox = test_utils.openChatBoxFor(contact_jid);
  102. chatboxview = converse.chatboxviews.get(contact_jid);
  103. spyOn(chatboxview, 'focus');
  104. // Test that they can be trimmed
  105. runs(function () {
  106. converse.rosterview.update(); // XXX: Hack to make sure $roster element is attaced.
  107. });
  108. waits(300); // ChatBox.show() is debounced for 250ms
  109. runs(function () {
  110. $el = converse.rosterview.$el.find('a.open-chat:contains("'+chatbox.get('fullname')+'")');
  111. jid = $el.text().replace(/ /g,'.').toLowerCase() + '@localhost';
  112. $el.click();
  113. expect(converse.chatboxes.length).toEqual(2);
  114. expect(chatboxview.focus).toHaveBeenCalled();
  115. });
  116. });
  117. it("can be saved to, and retrieved from, browserStorage", function () {
  118. spyOn(converse, 'emit');
  119. spyOn(this.chatboxviews, 'trimChats');
  120. runs(function () {
  121. test_utils.openControlBox();
  122. });
  123. waits(250);
  124. runs(function () {
  125. test_utils.openChatBoxes(6);
  126. expect(this.chatboxviews.trimChats).toHaveBeenCalled();
  127. // We instantiate a new ChatBoxes collection, which by default
  128. // will be empty.
  129. var newchatboxes = new this.ChatBoxes();
  130. expect(newchatboxes.length).toEqual(0);
  131. // The chatboxes will then be fetched from browserStorage inside the
  132. // onConnected method
  133. newchatboxes.onConnected();
  134. expect(newchatboxes.length).toEqual(7);
  135. // Check that the chatboxes items retrieved from browserStorage
  136. // have the same attributes values as the original ones.
  137. var attrs = ['id', 'box_id', 'visible'];
  138. var new_attrs, old_attrs;
  139. for (var i=0; i<attrs.length; i++) {
  140. new_attrs = _.pluck(_.pluck(newchatboxes.models, 'attributes'), attrs[i]);
  141. old_attrs = _.pluck(_.pluck(this.chatboxes.models, 'attributes'), attrs[i]);
  142. expect(_.isEqual(new_attrs, old_attrs)).toEqual(true);
  143. }
  144. this.rosterview.render();
  145. }.bind(converse));
  146. }.bind(converse));
  147. it("can be closed by clicking a DOM element with class 'close-chatbox-button'", function () {
  148. var chatbox = test_utils.openChatBoxes(1)[0],
  149. controlview = this.chatboxviews.get('controlbox'), // The controlbox is currently open
  150. chatview = this.chatboxviews.get(chatbox.get('jid'));
  151. spyOn(chatview, 'close').andCallThrough();
  152. spyOn(controlview, 'close').andCallThrough();
  153. spyOn(converse, 'emit');
  154. // We need to rebind all events otherwise our spy won't be called
  155. controlview.delegateEvents();
  156. chatview.delegateEvents();
  157. runs(function () {
  158. controlview.$el.find('.close-chatbox-button').click();
  159. });
  160. waits(250);
  161. runs(function () {
  162. expect(controlview.close).toHaveBeenCalled();
  163. expect(converse.emit).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object));
  164. expect(converse.emit.callCount, 1);
  165. chatview.$el.find('.close-chatbox-button').click();
  166. });
  167. waits(250);
  168. runs(function () {
  169. expect(chatview.close).toHaveBeenCalled();
  170. expect(converse.emit).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object));
  171. expect(converse.emit.callCount, 2);
  172. });
  173. }.bind(converse));
  174. it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'", function () {
  175. var chatbox = test_utils.openChatBoxes(1)[0],
  176. chatview = this.chatboxviews.get(chatbox.get('jid')),
  177. trimmed_chatboxes = this.minimized_chats,
  178. trimmedview;
  179. spyOn(chatview, 'minimize').andCallThrough();
  180. spyOn(converse, 'emit');
  181. // We need to rebind all events otherwise our spy won't be called
  182. chatview.delegateEvents();
  183. runs(function () {
  184. chatview.$el.find('.toggle-chatbox-button').click();
  185. });
  186. waits(250);
  187. runs(function () {
  188. expect(chatview.minimize).toHaveBeenCalled();
  189. expect(converse.emit).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object));
  190. expect(converse.emit.callCount, 2);
  191. expect(chatview.$el.is(':visible')).toBeFalsy();
  192. expect(chatview.model.get('minimized')).toBeTruthy();
  193. chatview.$el.find('.toggle-chatbox-button').click();
  194. trimmedview = trimmed_chatboxes.get(chatview.model.get('id'));
  195. spyOn(trimmedview, 'restore').andCallThrough();
  196. trimmedview.delegateEvents();
  197. trimmedview.$("a.restore-chat").click();
  198. });
  199. waits(250);
  200. runs(function () {
  201. expect(trimmedview.restore).toHaveBeenCalled();
  202. expect(converse.emit).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
  203. expect(chatview.$el.find('.chat-body').is(':visible')).toBeTruthy();
  204. expect(chatview.$el.find('.toggle-chatbox-button').hasClass('icon-minus')).toBeTruthy();
  205. expect(chatview.$el.find('.toggle-chatbox-button').hasClass('icon-plus')).toBeFalsy();
  206. expect(chatview.model.get('minimized')).toBeFalsy();
  207. });
  208. }.bind(converse));
  209. it("will be removed from browserStorage when closed", function () {
  210. spyOn(converse, 'emit');
  211. spyOn(converse.chatboxviews, 'trimChats');
  212. this.chatboxes.browserStorage._clear();
  213. runs(function () {
  214. test_utils.closeControlBox();
  215. });
  216. waits(50);
  217. runs(function () {
  218. expect(converse.emit).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object));
  219. expect(converse.chatboxes.length).toEqual(1);
  220. expect(converse.chatboxes.pluck('id')).toEqual(['controlbox']);
  221. test_utils.openChatBoxes(6);
  222. expect(converse.chatboxviews.trimChats).toHaveBeenCalled();
  223. expect(converse.chatboxes.length).toEqual(7);
  224. expect(converse.emit).toHaveBeenCalledWith('chatBoxOpened', jasmine.any(Object));
  225. test_utils.closeAllChatBoxes();
  226. });
  227. waits(50);
  228. runs(function () {
  229. expect(converse.chatboxes.length).toEqual(1);
  230. expect(converse.chatboxes.pluck('id')).toEqual(['controlbox']);
  231. expect(converse.emit).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object));
  232. var newchatboxes = new this.ChatBoxes();
  233. expect(newchatboxes.length).toEqual(0);
  234. expect(converse.chatboxes.pluck('id')).toEqual(['controlbox']);
  235. // onConnected will fetch chatboxes in browserStorage, but
  236. // because there aren't any open chatboxes, there won't be any
  237. // in browserStorage either. XXX except for the controlbox
  238. newchatboxes.onConnected();
  239. expect(newchatboxes.length).toEqual(1);
  240. expect(newchatboxes.models[0].id).toBe("controlbox");
  241. }.bind(converse));
  242. }.bind(converse));
  243. describe("A chat toolbar", function () {
  244. it("can be found on each chat box", function () {
  245. var contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
  246. test_utils.openChatBoxFor(contact_jid);
  247. var chatbox = this.chatboxes.get(contact_jid);
  248. var view = this.chatboxviews.get(contact_jid);
  249. expect(chatbox).toBeDefined();
  250. expect(view).toBeDefined();
  251. var $toolbar = view.$el.find('ul.chat-toolbar');
  252. expect($toolbar.length).toBe(1);
  253. expect($toolbar.children('li').length).toBe(3);
  254. }.bind(converse));
  255. it("contains a button for inserting emoticons", function () {
  256. var contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost',
  257. view, $toolbar, $textarea;
  258. waits(300); // ChatBox.show() is debounced for 250ms
  259. runs(function () {
  260. test_utils.openChatBoxFor(contact_jid);
  261. view = converse.chatboxviews.get(contact_jid);
  262. $toolbar = view.$el.find('ul.chat-toolbar');
  263. $textarea = view.$el.find('textarea.chat-textarea');
  264. expect($toolbar.children('li.toggle-smiley').length).toBe(1);
  265. // Register spies
  266. spyOn(view, 'toggleEmoticonMenu').andCallThrough();
  267. spyOn(view, 'insertEmoticon').andCallThrough();
  268. view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
  269. $toolbar.children('li.toggle-smiley').click();
  270. });
  271. waits(250);
  272. runs(function () {
  273. expect(view.toggleEmoticonMenu).toHaveBeenCalled();
  274. var $menu = view.$el.find('.toggle-smiley ul');
  275. var $items = $menu.children('li');
  276. expect($menu.is(':visible')).toBeTruthy();
  277. expect($items.length).toBe(13);
  278. expect($($items[0]).children('a').data('emoticon')).toBe(':)');
  279. expect($($items[1]).children('a').data('emoticon')).toBe(';)');
  280. expect($($items[2]).children('a').data('emoticon')).toBe(':D');
  281. expect($($items[3]).children('a').data('emoticon')).toBe(':P');
  282. expect($($items[4]).children('a').data('emoticon')).toBe('8)');
  283. expect($($items[5]).children('a').data('emoticon')).toBe('>:)');
  284. expect($($items[6]).children('a').data('emoticon')).toBe(':S');
  285. expect($($items[7]).children('a').data('emoticon')).toBe(':\\');
  286. expect($($items[8]).children('a').data('emoticon')).toBe('>:(');
  287. expect($($items[9]).children('a').data('emoticon')).toBe(':(');
  288. expect($($items[10]).children('a').data('emoticon')).toBe(':O');
  289. expect($($items[11]).children('a').data('emoticon')).toBe('(^.^)b');
  290. expect($($items[12]).children('a').data('emoticon')).toBe('<3');
  291. $items.first().click();
  292. });
  293. waits(250);
  294. runs(function () {
  295. expect(view.insertEmoticon).toHaveBeenCalled();
  296. expect($textarea.val()).toBe(':) ');
  297. expect(view.$el.find('.toggle-smiley ul').is(':visible')).toBeFalsy();
  298. $toolbar.children('li.toggle-smiley').click();
  299. });
  300. waits(250);
  301. runs(function () {
  302. expect(view.toggleEmoticonMenu).toHaveBeenCalled();
  303. expect(view.$el.find('.toggle-smiley ul').is(':visible')).toBeTruthy();
  304. view.$el.find('.toggle-smiley ul').children('li').last().click();
  305. });
  306. waits(250);
  307. runs(function () {
  308. expect(view.insertEmoticon).toHaveBeenCalled();
  309. expect(view.$el.find('.toggle-smiley ul').is(':visible')).toBeFalsy();
  310. expect($textarea.val()).toBe(':) <3 ');
  311. });
  312. });
  313. it("contains a button for starting an encrypted chat session", function () {
  314. // TODO: More tests can be added here...
  315. var contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
  316. test_utils.openChatBoxFor(contact_jid);
  317. var view = this.chatboxviews.get(contact_jid);
  318. var $toolbar = view.$el.find('ul.chat-toolbar');
  319. expect($toolbar.children('li.toggle-otr').length).toBe(1);
  320. // Register spies
  321. spyOn(view, 'toggleOTRMenu').andCallThrough();
  322. view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
  323. runs(function () {
  324. $toolbar.children('li.toggle-otr').click();
  325. });
  326. waits(250);
  327. runs(function () {
  328. expect(view.toggleOTRMenu).toHaveBeenCalled();
  329. var $menu = view.$el.find('.toggle-otr ul');
  330. expect($menu.is(':visible')).toBeTruthy();
  331. expect($menu.children('li').length).toBe(2);
  332. });
  333. }.bind(converse));
  334. it("can contain a button for starting a call", function () {
  335. var view, callButton, $toolbar;
  336. var contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
  337. spyOn(converse, 'emit');
  338. // First check that the button doesn't show if it's not enabled
  339. // via "visible_toolbar_buttons"
  340. converse.visible_toolbar_buttons.call = false;
  341. test_utils.openChatBoxFor(contact_jid);
  342. view = this.chatboxviews.get(contact_jid);
  343. $toolbar = view.$el.find('ul.chat-toolbar');
  344. callButton = $toolbar.find('.toggle-call');
  345. expect(callButton.length).toBe(0);
  346. view.close();
  347. // Now check that it's shown if enabled and that it emits
  348. // callButtonClicked
  349. converse.visible_toolbar_buttons.call = true; // enable the button
  350. test_utils.openChatBoxFor(contact_jid);
  351. view = this.chatboxviews.get(contact_jid);
  352. $toolbar = view.$el.find('ul.chat-toolbar');
  353. callButton = $toolbar.find('.toggle-call');
  354. expect(callButton.length).toBe(1);
  355. callButton.click();
  356. expect(converse.emit).toHaveBeenCalledWith('callButtonClicked', jasmine.any(Object));
  357. }.bind(converse));
  358. it("can contain a button for clearing messages", function () {
  359. var view, clearButton, $toolbar;
  360. var contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
  361. // First check that the button doesn't show if it's not enabled
  362. // via "visible_toolbar_buttons"
  363. converse.visible_toolbar_buttons.clear = false;
  364. test_utils.openChatBoxFor(contact_jid);
  365. view = this.chatboxviews.get(contact_jid);
  366. view = this.chatboxviews.get(contact_jid);
  367. $toolbar = view.$el.find('ul.chat-toolbar');
  368. clearButton = $toolbar.find('.toggle-clear');
  369. expect(clearButton.length).toBe(0);
  370. view.close();
  371. // Now check that it's shown if enabled and that it calls
  372. // clearMessages
  373. converse.visible_toolbar_buttons.clear = true; // enable the button
  374. test_utils.openChatBoxFor(contact_jid);
  375. view = this.chatboxviews.get(contact_jid);
  376. $toolbar = view.$el.find('ul.chat-toolbar');
  377. clearButton = $toolbar.find('.toggle-clear');
  378. expect(clearButton.length).toBe(1);
  379. spyOn(view, 'clearMessages');
  380. view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
  381. clearButton.click();
  382. expect(view.clearMessages).toHaveBeenCalled();
  383. }.bind(converse));
  384. }.bind(converse));
  385. describe("A Chat Message", function () {
  386. beforeEach(function () {
  387. runs(function () {
  388. test_utils.closeAllChatBoxes();
  389. });
  390. waits(250);
  391. runs(function () {});
  392. });
  393. it("can be received which will open a chatbox and be displayed inside it", function () {
  394. spyOn(converse, 'emit');
  395. var message = 'This is a received message';
  396. var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  397. var msg = $msg({
  398. from: sender_jid,
  399. to: this.connection.jid,
  400. type: 'chat',
  401. id: (new Date()).getTime()
  402. }).c('body').t(message).up()
  403. .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
  404. // We don't already have an open chatbox for this user
  405. expect(this.chatboxes.get(sender_jid)).not.toBeDefined();
  406. runs(function () {
  407. // onMessage is a handler for received XMPP messages
  408. this.chatboxes.onMessage(msg);
  409. expect(converse.emit).toHaveBeenCalledWith('message', msg);
  410. }.bind(converse));
  411. waits(50);
  412. runs(function () {
  413. // Check that the chatbox and its view now exist
  414. var chatbox = this.chatboxes.get(sender_jid);
  415. var chatboxview = this.chatboxviews.get(sender_jid);
  416. expect(chatbox).toBeDefined();
  417. expect(chatboxview).toBeDefined();
  418. // Check that the message was received and check the message parameters
  419. expect(chatbox.messages.length).toEqual(1);
  420. var msg_obj = chatbox.messages.models[0];
  421. expect(msg_obj.get('message')).toEqual(message);
  422. // XXX: This is stupid, fullname is actually only the
  423. // users first name
  424. expect(msg_obj.get('fullname')).toEqual(mock.cur_names[0].split(' ')[0]);
  425. expect(msg_obj.get('sender')).toEqual('them');
  426. expect(msg_obj.get('delayed')).toEqual(false);
  427. // Now check that the message appears inside the chatbox in the DOM
  428. var $chat_content = chatboxview.$el.find('.chat-content');
  429. var msg_txt = $chat_content.find('.chat-message').find('.chat-msg-content').text();
  430. expect(msg_txt).toEqual(message);
  431. var sender_txt = $chat_content.find('span.chat-msg-them').text();
  432. expect(sender_txt.match(/^[0-9][0-9]:[0-9][0-9] /)).toBeTruthy();
  433. }.bind(converse));
  434. }.bind(converse));
  435. it("is ignored if it's intended for a different resource", function () {
  436. // Send a message from a different resource
  437. spyOn(converse, 'log');
  438. var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  439. var msg = $msg({
  440. from: sender_jid,
  441. to: converse.bare_jid+'/'+"some-other-resource",
  442. type: 'chat',
  443. id: (new Date()).getTime()
  444. }).c('body').t("This message will not be shown").up()
  445. .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
  446. converse.chatboxes.onMessage(msg);
  447. expect(converse.log).toHaveBeenCalledWith(
  448. "Ignore incoming message intended for a different resource: dummy@localhost/some-other-resource", "info");
  449. });
  450. it("can be a carbon message, as defined in XEP-0280", function () {
  451. // Send a message from a different resource
  452. spyOn(converse, 'log');
  453. var msgtext = 'This is a carbon message';
  454. var sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
  455. var msg = $msg({
  456. 'from': converse.bare_jid,
  457. 'id': (new Date()).getTime(),
  458. 'to': converse.connection.jid,
  459. 'type': 'chat',
  460. 'xmlns': 'jabber:client'
  461. }).c('received', {'xmlns': 'urn:xmpp:carbons:2'})
  462. .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
  463. .c('message', {
  464. 'xmlns': 'jabber:client',
  465. 'from': sender_jid,
  466. 'to': converse.bare_jid+'/another-resource',
  467. 'type': 'chat'
  468. }).c('body').t(msgtext).tree();
  469. converse.chatboxes.onMessage(msg);
  470. // Check that the chatbox and its view now exist
  471. var chatbox = converse.chatboxes.get(sender_jid);
  472. var chatboxview = converse.chatboxviews.get(sender_jid);
  473. expect(chatbox).toBeDefined();
  474. expect(chatboxview).toBeDefined();
  475. // Check that the message was received and check the message parameters
  476. expect(chatbox.messages.length).toEqual(1);
  477. var msg_obj = chatbox.messages.models[0];
  478. expect(msg_obj.get('message')).toEqual(msgtext);
  479. expect(msg_obj.get('fullname')).toEqual(mock.cur_names[1].split(' ')[0]);
  480. expect(msg_obj.get('sender')).toEqual('them');
  481. expect(msg_obj.get('delayed')).toEqual(false);
  482. // Now check that the message appears inside the chatbox in the DOM
  483. var $chat_content = chatboxview.$el.find('.chat-content');
  484. var msg_txt = $chat_content.find('.chat-message').find('.chat-msg-content').text();
  485. expect(msg_txt).toEqual(msgtext);
  486. var sender_txt = $chat_content.find('span.chat-msg-them').text();
  487. expect(sender_txt.match(/^[0-9][0-9]:[0-9][0-9] /)).toBeTruthy();
  488. });
  489. it("can be a carbon message that this user sent from a different client, as defined in XEP-0280", function () {
  490. // Send a message from a different resource
  491. spyOn(converse, 'log');
  492. var msgtext = 'This is a sent carbon message';
  493. var recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@localhost';
  494. var msg = $msg({
  495. 'from': converse.bare_jid,
  496. 'id': (new Date()).getTime(),
  497. 'to': converse.connection.jid,
  498. 'type': 'chat',
  499. 'xmlns': 'jabber:client'
  500. }).c('sent', {'xmlns': 'urn:xmpp:carbons:2'})
  501. .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
  502. .c('message', {
  503. 'xmlns': 'jabber:client',
  504. 'from': converse.bare_jid+'/another-resource',
  505. 'to': recipient_jid,
  506. 'type': 'chat'
  507. }).c('body').t(msgtext).tree();
  508. converse.chatboxes.onMessage(msg);
  509. // Check that the chatbox and its view now exist
  510. var chatbox = converse.chatboxes.get(recipient_jid);
  511. var chatboxview = converse.chatboxviews.get(recipient_jid);
  512. expect(chatbox).toBeDefined();
  513. expect(chatboxview).toBeDefined();
  514. // Check that the message was received and check the message parameters
  515. expect(chatbox.messages.length).toEqual(1);
  516. var msg_obj = chatbox.messages.models[0];
  517. expect(msg_obj.get('message')).toEqual(msgtext);
  518. expect(msg_obj.get('fullname')).toEqual(mock.cur_names[5].split(' ')[0]);
  519. expect(msg_obj.get('sender')).toEqual('me');
  520. expect(msg_obj.get('delayed')).toEqual(false);
  521. // Now check that the message appears inside the chatbox in the DOM
  522. var $chat_content = chatboxview.$el.find('.chat-content');
  523. var msg_txt = $chat_content.find('.chat-message').find('.chat-msg-content').text();
  524. expect(msg_txt).toEqual(msgtext);
  525. });
  526. it("received for a minimized chat box will increment a counter on its header", function () {
  527. var contact_name = mock.cur_names[0];
  528. var contact_jid = contact_name.replace(/ /g,'.').toLowerCase() + '@localhost';
  529. spyOn(this, 'emit');
  530. test_utils.openChatBoxFor(contact_jid);
  531. var chatview = this.chatboxviews.get(contact_jid);
  532. expect(chatview.$el.is(':visible')).toBeTruthy();
  533. expect(chatview.model.get('minimized')).toBeFalsy();
  534. chatview.$el.find('.toggle-chatbox-button').click();
  535. expect(chatview.model.get('minimized')).toBeTruthy();
  536. var message = 'This message is sent to a minimized chatbox';
  537. var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  538. var msg = $msg({
  539. from: sender_jid,
  540. to: this.connection.jid,
  541. type: 'chat',
  542. id: (new Date()).getTime()
  543. }).c('body').t(message).up()
  544. .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
  545. this.chatboxes.onMessage(msg);
  546. expect(this.emit).toHaveBeenCalledWith('message', msg);
  547. var trimmed_chatboxes = this.minimized_chats;
  548. var trimmedview = trimmed_chatboxes.get(contact_jid);
  549. var $count = trimmedview.$el.find('.chat-head-message-count');
  550. expect(chatview.$el.is(':visible')).toBeFalsy();
  551. expect(trimmedview.model.get('minimized')).toBeTruthy();
  552. expect($count.is(':visible')).toBeTruthy();
  553. expect($count.html()).toBe('1');
  554. this.chatboxes.onMessage(
  555. $msg({
  556. from: mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost',
  557. to: this.connection.jid,
  558. type: 'chat',
  559. id: (new Date()).getTime()
  560. }).c('body').t('This message is also sent to a minimized chatbox').up()
  561. .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()
  562. );
  563. expect(chatview.$el.is(':visible')).toBeFalsy();
  564. expect(trimmedview.model.get('minimized')).toBeTruthy();
  565. $count = trimmedview.$el.find('.chat-head-message-count');
  566. expect($count.is(':visible')).toBeTruthy();
  567. expect($count.html()).toBe('2');
  568. trimmedview.$el.find('.restore-chat').click();
  569. expect(trimmed_chatboxes.keys().length).toBe(0);
  570. }.bind(converse));
  571. it("will indicate when it has a time difference of more than a day between it and its predecessor", function () {
  572. spyOn(converse, 'emit');
  573. var contact_name = mock.cur_names[1];
  574. var contact_jid = contact_name.replace(/ /g,'.').toLowerCase() + '@localhost';
  575. test_utils.openChatBoxFor(contact_jid);
  576. test_utils.clearChatBoxMessages(contact_jid);
  577. var one_day_ago = moment();
  578. one_day_ago.subtract('days', 1);
  579. var message = 'This is a day old message';
  580. var chatbox = this.chatboxes.get(contact_jid);
  581. var chatboxview = this.chatboxviews.get(contact_jid);
  582. var $chat_content = chatboxview.$el.find('.chat-content');
  583. var msg_obj;
  584. var msg_txt;
  585. var sender_txt;
  586. var msg = $msg({
  587. from: contact_jid,
  588. to: this.connection.jid,
  589. type: 'chat',
  590. id: one_day_ago.unix()
  591. }).c('body').t(message).up()
  592. .c('delay', { xmlns:'urn:xmpp:delay', from: 'localhost', stamp: one_day_ago.format() })
  593. .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
  594. this.chatboxes.onMessage(msg);
  595. expect(converse.emit).toHaveBeenCalledWith('message', msg);
  596. expect(chatbox.messages.length).toEqual(1);
  597. msg_obj = chatbox.messages.models[0];
  598. expect(msg_obj.get('message')).toEqual(message);
  599. expect(msg_obj.get('fullname')).toEqual(contact_name.split(' ')[0]);
  600. expect(msg_obj.get('sender')).toEqual('them');
  601. expect(msg_obj.get('delayed')).toEqual(true);
  602. msg_txt = $chat_content.find('.chat-message').find('.chat-msg-content').text();
  603. expect(msg_txt).toEqual(message);
  604. sender_txt = $chat_content.find('span.chat-msg-them').text();
  605. expect(sender_txt.match(/^[0-9][0-9]:[0-9][0-9] /)).toBeTruthy();
  606. message = 'This is a current message';
  607. msg = $msg({
  608. from: contact_jid,
  609. to: this.connection.jid,
  610. type: 'chat',
  611. id: new Date().getTime()
  612. }).c('body').t(message).up()
  613. .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
  614. this.chatboxes.onMessage(msg);
  615. expect(converse.emit).toHaveBeenCalledWith('message', msg);
  616. // Check that there is a <time> element, with the required
  617. // props.
  618. var $time = $chat_content.find('time');
  619. var message_date = new Date();
  620. expect($time.length).toEqual(1);
  621. expect($time.attr('class')).toEqual('chat-info chat-date');
  622. expect($time.data('isodate')).toEqual(moment(message_date).format());
  623. expect($time.text()).toEqual(moment(message_date).format("dddd MMM Do YYYY"));
  624. // Normal checks for the 2nd message
  625. expect(chatbox.messages.length).toEqual(2);
  626. msg_obj = chatbox.messages.models[1];
  627. expect(msg_obj.get('message')).toEqual(message);
  628. expect(msg_obj.get('fullname')).toEqual(contact_name.split(' ')[0]);
  629. expect(msg_obj.get('sender')).toEqual('them');
  630. expect(msg_obj.get('delayed')).toEqual(false);
  631. msg_txt = $chat_content.find('.chat-message').last().find('.chat-msg-content').text();
  632. expect(msg_txt).toEqual(message);
  633. sender_txt = $chat_content.find('span.chat-msg-them').last().text();
  634. expect(sender_txt.match(/^[0-9][0-9]:[0-9][0-9] /)).toBeTruthy();
  635. }.bind(converse));
  636. it("can be sent from a chatbox, and will appear inside it", function () {
  637. spyOn(converse, 'emit');
  638. var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  639. test_utils.openChatBoxFor(contact_jid);
  640. expect(converse.emit).toHaveBeenCalledWith('chatBoxFocused', jasmine.any(Object));
  641. var view = this.chatboxviews.get(contact_jid);
  642. var message = 'This message is sent from this chatbox';
  643. spyOn(view, 'sendMessage').andCallThrough();
  644. test_utils.sendMessage(view, message);
  645. expect(view.sendMessage).toHaveBeenCalled();
  646. expect(view.model.messages.length, 2);
  647. expect(converse.emit.mostRecentCall.args, ['messageSend', message]);
  648. expect(view.$el.find('.chat-content').find('.chat-message').last().find('.chat-msg-content').text()).toEqual(message);
  649. }.bind(converse));
  650. it("is sanitized to prevent Javascript injection attacks", function () {
  651. var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  652. test_utils.openChatBoxFor(contact_jid);
  653. var view = this.chatboxviews.get(contact_jid);
  654. var message = '<p>This message contains <em>some</em> <b>markup</b></p>';
  655. spyOn(view, 'sendMessage').andCallThrough();
  656. test_utils.sendMessage(view, message);
  657. expect(view.sendMessage).toHaveBeenCalled();
  658. var msg = view.$el.find('.chat-content').find('.chat-message').last().find('.chat-msg-content');
  659. expect(msg.text()).toEqual(message);
  660. expect(msg.html()).toEqual('&lt;p&gt;This message contains &lt;em&gt;some&lt;/em&gt; &lt;b&gt;markup&lt;/b&gt;&lt;/p&gt;');
  661. }.bind(converse));
  662. it("can contain hyperlinks, which will be clickable", function () {
  663. var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  664. test_utils.openChatBoxFor(contact_jid);
  665. var view = this.chatboxviews.get(contact_jid);
  666. var message = 'This message contains a hyperlink: www.opkode.com';
  667. spyOn(view, 'sendMessage').andCallThrough();
  668. test_utils.sendMessage(view, message);
  669. expect(view.sendMessage).toHaveBeenCalled();
  670. var msg = view.$el.find('.chat-content').find('.chat-message').last().find('.chat-msg-content');
  671. expect(msg.text()).toEqual(message);
  672. expect(msg.html()).toEqual('This message contains a hyperlink: <a target="_blank" href="http://www.opkode.com">www.opkode.com</a>');
  673. }.bind(converse));
  674. it("should display emoticons correctly", function () {
  675. var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  676. test_utils.openChatBoxFor(contact_jid);
  677. var view = this.chatboxviews.get(contact_jid);
  678. var messages = [':)', ';)', ':D', ':P', '8)', '>:)', ':S', ':\\', '>:(', ':(', ':O', '(^.^)b', '<3'];
  679. var emoticons = [
  680. '<span class="emoticon icon-smiley"></span>', '<span class="emoticon icon-wink"></span>',
  681. '<span class="emoticon icon-grin"></span>', '<span class="emoticon icon-tongue"></span>',
  682. '<span class="emoticon icon-cool"></span>', '<span class="emoticon icon-evil"></span>',
  683. '<span class="emoticon icon-confused"></span>', '<span class="emoticon icon-wondering"></span>',
  684. '<span class="emoticon icon-angry"></span>', '<span class="emoticon icon-sad"></span>',
  685. '<span class="emoticon icon-shocked"></span>', '<span class="emoticon icon-thumbs-up"></span>',
  686. '<span class="emoticon icon-heart"></span>'
  687. ];
  688. spyOn(view, 'sendMessage').andCallThrough();
  689. for (var i = 0; i < messages.length; i++) {
  690. var message = messages[i];
  691. test_utils.sendMessage(view, message);
  692. expect(view.sendMessage).toHaveBeenCalled();
  693. var msg = view.$el.find('.chat-content').find('.chat-message').last().find('.chat-msg-content');
  694. expect(msg.html()).toEqual(emoticons[i]);
  695. }
  696. }.bind(converse));
  697. it("will have properly escaped URLs", function () {
  698. var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  699. test_utils.openChatBoxFor(contact_jid);
  700. var view = this.chatboxviews.get(contact_jid);
  701. spyOn(view, 'sendMessage').andCallThrough();
  702. var message = "http://www.opkode.com/'onmouseover='alert(1)'whatever";
  703. test_utils.sendMessage(view, message);
  704. expect(view.sendMessage).toHaveBeenCalled();
  705. var msg = view.$el.find('.chat-content').find('.chat-message').last().find('.chat-msg-content');
  706. expect(msg.text()).toEqual(message);
  707. expect(msg.html()).toEqual('<a target="_blank" href="http://www.opkode.com/%27onmouseover=%27alert%281%29%27whatever">http://www.opkode.com/\'onmouseover=\'alert(1)\'whatever</a>');
  708. message = 'http://www.opkode.com/"onmouseover="alert(1)"whatever';
  709. test_utils.sendMessage(view, message);
  710. expect(view.sendMessage).toHaveBeenCalled();
  711. msg = view.$el.find('.chat-content').find('.chat-message').last().find('.chat-msg-content');
  712. expect(msg.text()).toEqual(message);
  713. expect(msg.html()).toEqual('<a target="_blank" href="http://www.opkode.com/%22onmouseover=%22alert%281%29%22whatever">http://www.opkode.com/"onmouseover="alert(1)"whatever</a>');
  714. message = "https://en.wikipedia.org/wiki/Ender's_Game";
  715. test_utils.sendMessage(view, message);
  716. expect(view.sendMessage).toHaveBeenCalled();
  717. msg = view.$el.find('.chat-content').find('.chat-message').last().find('.chat-msg-content');
  718. expect(msg.text()).toEqual(message);
  719. expect(msg.html()).toEqual('<a target="_blank" href="https://en.wikipedia.org/wiki/Ender%27s_Game">https://en.wikipedia.org/wiki/Ender\'s_Game</a>');
  720. message = "https://en.wikipedia.org/wiki/Ender%27s_Game";
  721. test_utils.sendMessage(view, message);
  722. expect(view.sendMessage).toHaveBeenCalled();
  723. msg = view.$el.find('.chat-content').find('.chat-message').last().find('.chat-msg-content');
  724. expect(msg.text()).toEqual(message);
  725. expect(msg.html()).toEqual('<a target="_blank" href="https://en.wikipedia.org/wiki/Ender%27s_Game">https://en.wikipedia.org/wiki/Ender%27s_Game</a>');
  726. }.bind(converse));
  727. }.bind(converse));
  728. describe("An OTR Chat Message", function () {
  729. it("will not be carbon copied when it's sent out", function () {
  730. var msgtext = "?OTR,1,3,?OTR:AAIDAAAAAAEAAAABAAAAwCQ8HKsag0y0DGKsneo0kzKu1ua5L93M4UKTkCf1I2kbm2RgS5kIxDTxrTj3wVRB+H5Si86E1fKtuBgsDf/bKkGTM0h/49vh5lOD9HkE8cnSrFEn5GN,";
  731. var sender_jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@localhost';
  732. converse_api.chats.open(sender_jid);
  733. var chatbox = converse.chatboxes.get(sender_jid);
  734. spyOn(converse.connection, 'send');
  735. chatbox.set('otr_status', 1); // Set OTR status to UNVERIFIED, to mock an encrypted session
  736. chatbox.trigger('sendMessage', new converse.Message({ message: msgtext }));
  737. var $sent = $(converse.connection.send.argsForCall[0][0].tree());
  738. expect($sent.find('body').siblings('private').length).toBe(1);
  739. expect($sent.find('private').length).toBe(1);
  740. expect($sent.find('private').attr('xmlns')).toBe('urn:xmpp:carbons:2');
  741. chatbox.set('otr_status', 0); // Reset again to UNENCRYPTED
  742. });
  743. });
  744. describe("A Chat Status Notification", function () {
  745. it("does not open automatically if a chat state notification is received", function () {
  746. spyOn(converse, 'emit');
  747. var sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
  748. // <composing> state
  749. var msg = $msg({
  750. from: sender_jid,
  751. to: this.connection.jid,
  752. type: 'chat',
  753. id: (new Date()).getTime()
  754. }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
  755. this.chatboxes.onMessage(msg);
  756. expect(converse.emit).toHaveBeenCalledWith('message', msg);
  757. var chatboxview = this.chatboxviews.get(sender_jid);
  758. expect(chatboxview).toBeDefined();
  759. expect(chatboxview.$el.is(':visible')).toBeFalsy(); // The chat box is not visible
  760. }.bind(converse));
  761. describe("An active notification", function () {
  762. it("is sent when the user opens a chat box", function () {
  763. waits(300); // ChatBox.show() is debounced for 250ms
  764. runs(function () {
  765. spyOn(converse.connection, 'send');
  766. var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  767. test_utils.openChatBoxFor(contact_jid);
  768. var view = converse.chatboxviews.get(contact_jid);
  769. expect(view.model.get('chat_state')).toBe('active');
  770. expect(converse.connection.send).toHaveBeenCalled();
  771. var $stanza = $(converse.connection.send.argsForCall[0][0].tree());
  772. expect($stanza.attr('to')).toBe(contact_jid);
  773. expect($stanza.children().length).toBe(1);
  774. expect($stanza.children().prop('tagName')).toBe('active');
  775. });
  776. });
  777. it("is sent when the user maximizes a minimized a chat box", function () {
  778. var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  779. test_utils.openChatBoxFor(contact_jid);
  780. var view = this.chatboxviews.get(contact_jid);
  781. view.minimize();
  782. expect(view.model.get('chat_state')).toBe('inactive');
  783. spyOn(converse.connection, 'send');
  784. view.maximize();
  785. expect(view.model.get('chat_state')).toBe('active');
  786. expect(converse.connection.send).toHaveBeenCalled();
  787. var $stanza = $(converse.connection.send.argsForCall[0][0].tree());
  788. expect($stanza.attr('to')).toBe(contact_jid);
  789. expect($stanza.children().length).toBe(1);
  790. expect($stanza.children().prop('tagName')).toBe('active');
  791. }.bind(converse));
  792. }.bind(converse));
  793. describe("A composing notification", function () {
  794. it("is sent as soon as the user starts typing a message which is not a command", function () {
  795. waits(300); // ChatBox.show() is debounced for 250ms
  796. runs(function () {
  797. var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  798. test_utils.openChatBoxFor(contact_jid);
  799. var view = converse.chatboxviews.get(contact_jid);
  800. expect(view.model.get('chat_state')).toBe('active');
  801. spyOn(converse.connection, 'send');
  802. view.keyPressed({
  803. target: view.$el.find('textarea.chat-textarea'),
  804. keyCode: 1
  805. });
  806. expect(view.model.get('chat_state')).toBe('composing');
  807. expect(converse.connection.send).toHaveBeenCalled();
  808. var $stanza = $(converse.connection.send.argsForCall[0][0].tree());
  809. expect($stanza.attr('to')).toBe(contact_jid);
  810. expect($stanza.children().length).toBe(1);
  811. expect($stanza.children().prop('tagName')).toBe('composing');
  812. // The notification is not sent again
  813. view.keyPressed({
  814. target: view.$el.find('textarea.chat-textarea'),
  815. keyCode: 1
  816. });
  817. expect(view.model.get('chat_state')).toBe('composing');
  818. expect(converse.emit.callCount, 1);
  819. });
  820. });
  821. it("will be shown if received", function () {
  822. // See XEP-0085 http://xmpp.org/extensions/xep-0085.html#definitions
  823. spyOn(converse, 'emit');
  824. var sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
  825. // <composing> state
  826. var msg = $msg({
  827. from: sender_jid,
  828. to: this.connection.jid,
  829. type: 'chat',
  830. id: (new Date()).getTime()
  831. }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
  832. this.chatboxes.onMessage(msg);
  833. expect(converse.emit).toHaveBeenCalledWith('message', msg);
  834. var chatboxview = this.chatboxviews.get(sender_jid);
  835. expect(chatboxview).toBeDefined();
  836. // Check that the notification appears inside the chatbox in the DOM
  837. var $events = chatboxview.$el.find('.chat-event');
  838. expect($events.length).toBe(1);
  839. expect($events.text()).toEqual(mock.cur_names[1].split(' ')[0] + ' is typing');
  840. }.bind(converse));
  841. }.bind(converse));
  842. describe("A paused notification", function () {
  843. it("is sent if the user has stopped typing since 30 seconds", function () {
  844. var view, contact_jid;
  845. converse.TIMEOUTS.PAUSED = 200; // Make the timeout shorter so that we can test
  846. waits(300); // ChatBox.show() is debounced for 250ms
  847. runs(function () {
  848. contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  849. test_utils.openChatBoxFor(contact_jid);
  850. view = converse.chatboxviews.get(contact_jid);
  851. spyOn(converse.connection, 'send');
  852. spyOn(view, 'setChatState').andCallThrough();
  853. expect(view.model.get('chat_state')).toBe('active');
  854. view.keyPressed({
  855. target: view.$el.find('textarea.chat-textarea'),
  856. keyCode: 1
  857. });
  858. expect(view.model.get('chat_state')).toBe('composing');
  859. expect(converse.connection.send).toHaveBeenCalled();
  860. var $stanza = $(converse.connection.send.argsForCall[0][0].tree());
  861. expect($stanza.children().prop('tagName')).toBe('composing');
  862. });
  863. waits(250);
  864. runs(function () {
  865. expect(view.model.get('chat_state')).toBe('paused');
  866. expect(converse.connection.send).toHaveBeenCalled();
  867. var $stanza = $(converse.connection.send.argsForCall[1][0].tree());
  868. expect($stanza.attr('to')).toBe(contact_jid);
  869. expect($stanza.children().length).toBe(1);
  870. expect($stanza.children().prop('tagName')).toBe('paused');
  871. // Test #359. A paused notification should not be sent
  872. // out if the user simply types longer than the
  873. // timeout.
  874. view.keyPressed({
  875. target: view.$el.find('textarea.chat-textarea'),
  876. keyCode: 1
  877. });
  878. expect(view.setChatState).toHaveBeenCalled();
  879. expect(view.model.get('chat_state')).toBe('composing');
  880. });
  881. waits(100);
  882. runs(function () {
  883. view.keyPressed({
  884. target: view.$el.find('textarea.chat-textarea'),
  885. keyCode: 1
  886. });
  887. expect(view.model.get('chat_state')).toBe('composing');
  888. });
  889. waits(150);
  890. runs(function () {
  891. expect(view.model.get('chat_state')).toBe('composing');
  892. });
  893. });
  894. it("will be shown if received", function () {
  895. // TODO: only show paused state if the previous state was composing
  896. // See XEP-0085 http://xmpp.org/extensions/xep-0085.html#definitions
  897. spyOn(converse, 'emit');
  898. var sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
  899. // <paused> state
  900. var msg = $msg({
  901. from: sender_jid,
  902. to: this.connection.jid,
  903. type: 'chat',
  904. id: (new Date()).getTime()
  905. }).c('body').c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
  906. this.chatboxes.onMessage(msg);
  907. expect(converse.emit).toHaveBeenCalledWith('message', msg);
  908. var chatboxview = this.chatboxviews.get(sender_jid);
  909. var $events = chatboxview.$el.find('.chat-event');
  910. expect($events.length).toBe(1);
  911. expect($events.text()).toEqual(mock.cur_names[1].split(' ')[0] + ' has stopped typing');
  912. }.bind(converse));
  913. }.bind(converse));
  914. describe("An inactive notifciation", function () {
  915. it("is sent if the user has stopped typing since 2 minutes", function () {
  916. // Make the timeouts shorter so that we can test
  917. this.TIMEOUTS.PAUSED = 200;
  918. this.TIMEOUTS.INACTIVE = 200;
  919. var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  920. test_utils.openChatBoxFor(contact_jid);
  921. var view = this.chatboxviews.get(contact_jid);
  922. runs(function () {
  923. expect(view.model.get('chat_state')).toBe('active');
  924. view.keyPressed({
  925. target: view.$el.find('textarea.chat-textarea'),
  926. keyCode: 1
  927. });
  928. expect(view.model.get('chat_state')).toBe('composing');
  929. });
  930. waits(250);
  931. runs(function () {
  932. expect(view.model.get('chat_state')).toBe('paused');
  933. spyOn(converse.connection, 'send');
  934. });
  935. waits(250);
  936. runs(function () {
  937. expect(view.model.get('chat_state')).toBe('inactive');
  938. expect(converse.connection.send).toHaveBeenCalled();
  939. var $stanza = $(converse.connection.send.argsForCall[0][0].tree());
  940. expect($stanza.attr('to')).toBe(contact_jid);
  941. expect($stanza.children().length).toBe(1);
  942. expect($stanza.children().prop('tagName')).toBe('inactive');
  943. });
  944. }.bind(converse));
  945. it("is sent when the user a minimizes a chat box", function () {
  946. var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  947. test_utils.openChatBoxFor(contact_jid);
  948. var view = this.chatboxviews.get(contact_jid);
  949. spyOn(converse.connection, 'send');
  950. view.minimize();
  951. expect(view.model.get('chat_state')).toBe('inactive');
  952. expect(converse.connection.send).toHaveBeenCalled();
  953. var $stanza = $(converse.connection.send.argsForCall[0][0].tree());
  954. expect($stanza.attr('to')).toBe(contact_jid);
  955. expect($stanza.children().length).toBe(1);
  956. expect($stanza.children().prop('tagName')).toBe('inactive');
  957. }.bind(converse));
  958. it("is sent if the user closes a chat box", function () {
  959. waits(300); // ChatBox.show() is debounced for 250ms
  960. runs(function () {
  961. var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  962. test_utils.openChatBoxFor(contact_jid);
  963. var view = converse.chatboxviews.get(contact_jid);
  964. expect(view.model.get('chat_state')).toBe('active');
  965. spyOn(converse.connection, 'send');
  966. view.close();
  967. expect(view.model.get('chat_state')).toBe('inactive');
  968. expect(converse.connection.send).toHaveBeenCalled();
  969. var $stanza = $(converse.connection.send.argsForCall[0][0].tree());
  970. expect($stanza.attr('to')).toBe(contact_jid);
  971. expect($stanza.children().length).toBe(1);
  972. expect($stanza.children().prop('tagName')).toBe('inactive');
  973. });
  974. });
  975. it("will clear any other chat status notifications if its received", function () {
  976. // See XEP-0085 http://xmpp.org/extensions/xep-0085.html#definitions
  977. spyOn(converse, 'emit');
  978. var sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
  979. test_utils.openChatBoxFor(sender_jid);
  980. var view = this.chatboxviews.get(sender_jid);
  981. expect(view.$el.find('.chat-event').length).toBe(0);
  982. view.showStatusNotification(sender_jid+' '+'is typing');
  983. expect(view.$el.find('.chat-event').length).toBe(1);
  984. var msg = $msg({
  985. from: sender_jid,
  986. to: this.connection.jid,
  987. type: 'chat',
  988. id: (new Date()).getTime()
  989. }).c('body').c('inactive', {'xmlns': Strophe.NS.CHATSTATES}).tree();
  990. this.chatboxes.onMessage(msg);
  991. expect(converse.emit).toHaveBeenCalledWith('message', msg);
  992. expect(view.$el.find('.chat-event').length).toBe(0);
  993. }.bind(converse));
  994. }.bind(converse));
  995. describe("A gone notifciation", function () {
  996. it("will be shown if received", function () {
  997. spyOn(converse, 'emit');
  998. var sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
  999. // <paused> state
  1000. var msg = $msg({
  1001. from: sender_jid,
  1002. to: this.connection.jid,
  1003. type: 'chat',
  1004. id: (new Date()).getTime()
  1005. }).c('body').c('gone', {'xmlns': Strophe.NS.CHATSTATES}).tree();
  1006. this.chatboxes.onMessage(msg);
  1007. expect(converse.emit).toHaveBeenCalledWith('message', msg);
  1008. var chatboxview = this.chatboxviews.get(sender_jid);
  1009. var $events = chatboxview.$el.find('.chat-event');
  1010. expect($events.length).toBe(1);
  1011. expect($events.text()).toEqual(mock.cur_names[1].split(' ')[0] + ' has gone away');
  1012. }.bind(converse));
  1013. }.bind(converse));
  1014. }.bind(converse));
  1015. }.bind(converse));
  1016. describe("Special Messages", function () {
  1017. beforeEach(function () {
  1018. test_utils.closeAllChatBoxes();
  1019. test_utils.removeControlBox();
  1020. converse.roster.browserStorage._clear();
  1021. test_utils.initConverse();
  1022. test_utils.createContacts('current');
  1023. test_utils.openControlBox();
  1024. test_utils.openContactsPanel();
  1025. });
  1026. it("'/clear' can be used to clear messages in a conversation", function () {
  1027. spyOn(converse, 'emit');
  1028. var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  1029. test_utils.openChatBoxFor(contact_jid);
  1030. var view = this.chatboxviews.get(contact_jid);
  1031. var message = 'This message is another sent from this chatbox';
  1032. // Lets make sure there is at least one message already
  1033. // (e.g for when this test is run on its own).
  1034. test_utils.sendMessage(view, message);
  1035. expect(view.model.messages.length > 0).toBeTruthy();
  1036. expect(view.model.messages.browserStorage.records.length > 0).toBeTruthy();
  1037. expect(converse.emit).toHaveBeenCalledWith('messageSend', message);
  1038. message = '/clear';
  1039. spyOn(view, 'onMessageSubmitted').andCallThrough();
  1040. spyOn(view, 'clearMessages').andCallThrough();
  1041. spyOn(window, 'confirm').andCallFake(function () {
  1042. return true;
  1043. });
  1044. test_utils.sendMessage(view, message);
  1045. expect(view.onMessageSubmitted).toHaveBeenCalled();
  1046. expect(view.clearMessages).toHaveBeenCalled();
  1047. expect(window.confirm).toHaveBeenCalled();
  1048. expect(view.model.messages.length, 0); // The messages must be removed from the chatbox
  1049. expect(view.model.messages.browserStorage.records.length, 0); // And also from browserStorage
  1050. expect(converse.emit.callCount, 1);
  1051. expect(converse.emit.mostRecentCall.args, ['messageSend', message]);
  1052. }.bind(converse));
  1053. }.bind(converse));
  1054. describe("A Message Counter", function () {
  1055. beforeEach(function () {
  1056. converse.clearMsgCounter();
  1057. }.bind(converse));
  1058. it("is incremented when the message is received and the window is not focused", function () {
  1059. spyOn(converse, 'emit');
  1060. expect(this.msg_counter).toBe(0);
  1061. spyOn(converse, 'incrementMsgCounter').andCallThrough();
  1062. $(window).trigger('blur');
  1063. var message = 'This message will increment the message counter';
  1064. var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost',
  1065. msg = $msg({
  1066. from: sender_jid,
  1067. to: this.connection.jid,
  1068. type: 'chat',
  1069. id: (new Date()).getTime()
  1070. }).c('body').t(message).up()
  1071. .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
  1072. this.chatboxes.onMessage(msg);
  1073. expect(converse.incrementMsgCounter).toHaveBeenCalled();
  1074. expect(this.msg_counter).toBe(1);
  1075. expect(converse.emit).toHaveBeenCalledWith('message', msg);
  1076. }.bind(converse));
  1077. it("is cleared when the window is focused", function () {
  1078. spyOn(converse, 'clearMsgCounter').andCallThrough();
  1079. runs(function () {
  1080. $(window).triggerHandler('blur');
  1081. $(window).triggerHandler('focus');
  1082. });
  1083. waits(50);
  1084. runs(function () {
  1085. expect(converse.clearMsgCounter).toHaveBeenCalled();
  1086. });
  1087. }.bind(converse));
  1088. it("is not incremented when the message is received and the window is focused", function () {
  1089. expect(this.msg_counter).toBe(0);
  1090. spyOn(converse, 'incrementMsgCounter').andCallThrough();
  1091. $(window).trigger('focus');
  1092. var message = 'This message will not increment the message counter';
  1093. var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost',
  1094. msg = $msg({
  1095. from: sender_jid,
  1096. to: this.connection.jid,
  1097. type: 'chat',
  1098. id: (new Date()).getTime()
  1099. }).c('body').t(message).up()
  1100. .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
  1101. this.chatboxes.onMessage(msg);
  1102. expect(converse.incrementMsgCounter).not.toHaveBeenCalled();
  1103. expect(this.msg_counter).toBe(0);
  1104. }.bind(converse));
  1105. }.bind(converse));
  1106. }, converse, mock, test_utils));
  1107. }));