chatroom.js 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969
  1. /*global converse */
  2. (function (root, factory) {
  3. define([
  4. "jquery",
  5. "underscore",
  6. "mock",
  7. "test_utils",
  8. "utils"
  9. ], function ($, _, mock, test_utils, utils) {
  10. return factory($, _, mock, test_utils, utils);
  11. }
  12. );
  13. } (this, function ($, _, mock, test_utils, utils) {
  14. var $pres = converse_api.env.$pres;
  15. var $iq = converse_api.env.$iq;
  16. var $msg = converse_api.env.$msg;
  17. var Strophe = converse_api.env.Strophe;
  18. return describe("ChatRooms", function (mock, test_utils) {
  19. beforeEach(function () {
  20. runs(function () {
  21. test_utils.closeAllChatBoxes();
  22. test_utils.clearBrowserStorage();
  23. });
  24. });
  25. describe("The \"rooms\" API", function () {
  26. beforeEach(function () {
  27. test_utils.closeAllChatBoxes();
  28. test_utils.clearBrowserStorage();
  29. converse.rosterview.model.reset();
  30. test_utils.createContacts('current');
  31. });
  32. it("has a method 'close' which closes rooms by JID or all rooms when called with no arguments", function () {
  33. runs(function () {
  34. test_utils.openChatRoom('lounge', 'localhost', 'dummy');
  35. test_utils.openChatRoom('leisure', 'localhost', 'dummy');
  36. test_utils.openChatRoom('news', 'localhost', 'dummy');
  37. expect(converse.chatboxviews.get('lounge@localhost').$el.is(':visible')).toBeTruthy();
  38. expect(converse.chatboxviews.get('leisure@localhost').$el.is(':visible')).toBeTruthy();
  39. expect(converse.chatboxviews.get('news@localhost').$el.is(':visible')).toBeTruthy();
  40. });
  41. waits('100');
  42. runs(function () {
  43. converse_api.rooms.close('lounge@localhost');
  44. expect(converse.chatboxviews.get('lounge@localhost')).toBeUndefined();
  45. expect(converse.chatboxviews.get('leisure@localhost').$el.is(':visible')).toBeTruthy();
  46. expect(converse.chatboxviews.get('news@localhost').$el.is(':visible')).toBeTruthy();
  47. converse_api.rooms.close(['leisure@localhost', 'news@localhost']);
  48. expect(converse.chatboxviews.get('lounge@localhost')).toBeUndefined();
  49. expect(converse.chatboxviews.get('leisure@localhost')).toBeUndefined();
  50. expect(converse.chatboxviews.get('news@localhost')).toBeUndefined();
  51. test_utils.openChatRoom('lounge', 'localhost', 'dummy');
  52. test_utils.openChatRoom('leisure', 'localhost', 'dummy');
  53. expect(converse.chatboxviews.get('lounge@localhost').$el.is(':visible')).toBeTruthy();
  54. expect(converse.chatboxviews.get('leisure@localhost').$el.is(':visible')).toBeTruthy();
  55. });
  56. waits('100');
  57. runs(function () {
  58. converse_api.rooms.close();
  59. expect(converse.chatboxviews.get('lounge@localhost')).toBeUndefined();
  60. expect(converse.chatboxviews.get('leisure@localhost')).toBeUndefined();
  61. });
  62. });
  63. it("has a method 'get' which returns a wrapped chat room (if it exists)", function () {
  64. waits('300'); // ChatBox.show() is debounced for 250ms
  65. runs(function () {
  66. test_utils.openChatRoom('lounge', 'localhost', 'dummy');
  67. var jid = 'lounge@localhost';
  68. var room = converse_api.rooms.get(jid);
  69. expect(room instanceof Object).toBeTruthy();
  70. expect(room.is_chatroom).toBeTruthy();
  71. var chatroomview = converse.chatboxviews.get(jid);
  72. expect(chatroomview.$el.is(':visible')).toBeTruthy();
  73. chatroomview.close();
  74. });
  75. waits('300'); // ChatBox.show() is debounced for 250ms
  76. runs(function () {
  77. // Test with mixed case
  78. test_utils.openChatRoom('Leisure', 'localhost', 'dummy');
  79. var jid = 'Leisure@localhost';
  80. var room = converse_api.rooms.get(jid);
  81. expect(room instanceof Object).toBeTruthy();
  82. var chatroomview = converse.chatboxviews.get(jid.toLowerCase());
  83. expect(chatroomview.$el.is(':visible')).toBeTruthy();
  84. });
  85. waits('300'); // ChatBox.show() is debounced for 250ms
  86. runs(function () {
  87. var jid = 'leisure@localhost';
  88. var room = converse_api.rooms.get(jid);
  89. expect(room instanceof Object).toBeTruthy();
  90. var chatroomview = converse.chatboxviews.get(jid.toLowerCase());
  91. expect(chatroomview.$el.is(':visible')).toBeTruthy();
  92. jid = 'leiSure@localhost';
  93. room = converse_api.rooms.get(jid);
  94. expect(room instanceof Object).toBeTruthy();
  95. chatroomview = converse.chatboxviews.get(jid.toLowerCase());
  96. expect(chatroomview.$el.is(':visible')).toBeTruthy();
  97. chatroomview.close();
  98. // Non-existing room
  99. jid = 'lounge2@localhost';
  100. room = converse_api.rooms.get(jid);
  101. expect(typeof room === 'undefined').toBeTruthy();
  102. });
  103. });
  104. it("has a method 'open' which opens and returns a wrapped chat box", function () {
  105. var chatroomview;
  106. var jid = 'lounge@localhost';
  107. var room = converse_api.rooms.open(jid);
  108. runs(function () {
  109. // Test on chat room that doesn't exist.
  110. expect(room instanceof Object).toBeTruthy();
  111. expect(room.is_chatroom).toBeTruthy();
  112. chatroomview = converse.chatboxviews.get(jid);
  113. expect(chatroomview.$el.is(':visible')).toBeTruthy();
  114. });
  115. waits('300'); // ChatBox.show() is debounced for 250ms
  116. runs(function () {
  117. // Test again, now that the room exists.
  118. room = converse_api.rooms.open(jid);
  119. expect(room instanceof Object).toBeTruthy();
  120. expect(room.is_chatroom).toBeTruthy();
  121. chatroomview = converse.chatboxviews.get(jid);
  122. expect(chatroomview.$el.is(':visible')).toBeTruthy();
  123. });
  124. waits('300'); // ChatBox.show() is debounced for 250ms
  125. runs(function () {
  126. // Test with mixed case in JID
  127. jid = 'Leisure@localhost';
  128. room = converse_api.rooms.open(jid);
  129. expect(room instanceof Object).toBeTruthy();
  130. chatroomview = converse.chatboxviews.get(jid.toLowerCase());
  131. expect(chatroomview.$el.is(':visible')).toBeTruthy();
  132. jid = 'leisure@localhost';
  133. room = converse_api.rooms.open(jid);
  134. expect(room instanceof Object).toBeTruthy();
  135. chatroomview = converse.chatboxviews.get(jid.toLowerCase());
  136. expect(chatroomview.$el.is(':visible')).toBeTruthy();
  137. jid = 'leiSure@localhost';
  138. room = converse_api.rooms.open(jid);
  139. expect(room instanceof Object).toBeTruthy();
  140. chatroomview = converse.chatboxviews.get(jid.toLowerCase());
  141. expect(chatroomview.$el.is(':visible')).toBeTruthy();
  142. chatroomview.close();
  143. });
  144. });
  145. });
  146. describe("A Chat Room", function () {
  147. beforeEach(function () {
  148. runs(function () {
  149. test_utils.closeAllChatBoxes();
  150. test_utils.clearBrowserStorage();
  151. });
  152. });
  153. it("can have spaces and special characters in its name", function () {
  154. test_utils.openChatRoom('lounge & leisure', 'localhost', 'dummy');
  155. var view = converse.chatboxviews.get(
  156. Strophe.escapeNode('lounge & leisure')+'@localhost');
  157. expect(view instanceof converse.ChatRoomView).toBe(true);
  158. });
  159. it("shows users currently present in the room", function () {
  160. test_utils.openAndEnterChatRoom('lounge', 'localhost', 'dummy');
  161. var name;
  162. var view = this.chatboxviews.get('lounge@localhost'),
  163. $occupants = view.$('.occupant-list');
  164. spyOn(view, 'onChatRoomPresence').andCallThrough();
  165. var presence, role;
  166. for (var i=0; i<mock.chatroom_names.length; i++) {
  167. name = mock.chatroom_names[i];
  168. role = mock.chatroom_roles[name].role;
  169. // See example 21 http://xmpp.org/extensions/xep-0045.html#enter-pres
  170. presence = $pres({
  171. to:'dummy@localhost/pda',
  172. from:'lounge@localhost/'+name
  173. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  174. .c('item').attrs({
  175. affiliation: mock.chatroom_roles[name].affiliation,
  176. jid: name.replace(/ /g,'.').toLowerCase() + '@localhost',
  177. role: role
  178. }).up()
  179. .c('status').attrs({code:'110'}).nodeTree;
  180. converse.connection._dataRecv(test_utils.createRequest(presence));
  181. expect(view.onChatRoomPresence).toHaveBeenCalled();
  182. expect($occupants.find('li').length).toBe(2+i);
  183. expect($($occupants.find('li')[i+1]).text()).toBe(mock.chatroom_names[i]);
  184. expect($($occupants.find('li')[i+1]).hasClass('moderator')).toBe(role === "moderator");
  185. }
  186. // Test users leaving the room
  187. // http://xmpp.org/extensions/xep-0045.html#exit
  188. for (i=mock.chatroom_names.length-1; i>-1; i--) {
  189. name = mock.chatroom_names[i];
  190. role = mock.chatroom_roles[name].role;
  191. // See example 21 http://xmpp.org/extensions/xep-0045.html#enter-pres
  192. presence = $pres({
  193. to:'dummy@localhost/pda',
  194. from:'lounge@localhost/'+name,
  195. type: 'unavailable'
  196. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  197. .c('item').attrs({
  198. affiliation: mock.chatroom_roles[name].affiliation,
  199. jid: name.replace(/ /g,'.').toLowerCase() + '@localhost',
  200. role: 'none'
  201. }).nodeTree;
  202. converse.connection._dataRecv(test_utils.createRequest(presence));
  203. expect(view.onChatRoomPresence).toHaveBeenCalled();
  204. expect($occupants.find('li').length).toBe(i+1);
  205. }
  206. }.bind(converse));
  207. it("indicates moderators by means of a special css class and tooltip", function () {
  208. test_utils.openAndEnterChatRoom('lounge', 'localhost', 'dummy');
  209. var view = converse.chatboxviews.get('lounge@localhost');
  210. var presence = $pres({
  211. to:'dummy@localhost/pda',
  212. from:'lounge@localhost/moderatorman'
  213. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  214. .c('item').attrs({
  215. affiliation: 'admin',
  216. jid: name.replace(/ /g,'.').toLowerCase() + '@localhost',
  217. role: 'moderator',
  218. }).up()
  219. .c('status').attrs({code:'110'}).nodeTree;
  220. converse.connection._dataRecv(test_utils.createRequest(presence));
  221. var occupant = view.$el.find('.occupant-list').find('li');
  222. expect(occupant.length).toBe(2);
  223. expect($(occupant).first().text()).toBe("dummy");
  224. expect($(occupant).last().text()).toBe("moderatorman");
  225. expect($(occupant).last().attr('class').indexOf('moderator')).not.toBe(-1);
  226. expect($(occupant).last().attr('title')).toBe('This user is a moderator. Click to mention this user in your message.');
  227. });
  228. it("will use the user's reserved nickname, if it exists", function () {
  229. var sent_IQ, IQ_id;
  230. var sendIQ = converse.connection.sendIQ;
  231. spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) {
  232. sent_IQ = iq;
  233. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  234. });
  235. test_utils.openChatRoom('lounge', 'localhost', 'dummy');
  236. var view = converse.chatboxviews.get('lounge@localhost');
  237. spyOn(view, 'join').andCallThrough();
  238. /* <iq from='hag66@shakespeare.lit/pda'
  239. * id='getnick1'
  240. * to='coven@chat.shakespeare.lit'
  241. * type='get'>
  242. * <query xmlns='http://jabber.org/protocol/disco#info'
  243. * node='x-roomuser-item'/>
  244. * </iq>
  245. */
  246. expect(sent_IQ.toLocaleString()).toBe(
  247. "<iq to='lounge@localhost' from='dummy@localhost/resource' "+
  248. "type='get' xmlns='jabber:client' id='"+IQ_id+"'>"+
  249. "<query xmlns='http://jabber.org/protocol/disco#info' node='x-roomuser-item'/></iq>"
  250. );
  251. /* <iq from='coven@chat.shakespeare.lit'
  252. * id='getnick1'
  253. * to='hag66@shakespeare.lit/pda'
  254. * type='result'>
  255. * <query xmlns='http://jabber.org/protocol/disco#info'
  256. * node='x-roomuser-item'>
  257. * <identity
  258. * category='conference'
  259. * name='thirdwitch'
  260. * type='text'/>
  261. * </query>
  262. * </iq>
  263. */
  264. var stanza = $iq({
  265. 'type': 'result',
  266. 'id': IQ_id,
  267. 'from': view.model.get('jid'),
  268. 'to': converse.connection.jid
  269. }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info', 'node': 'x-roomuser-item'})
  270. .c('identity', {'category': 'conference', 'name': 'thirdwitch', 'type': 'text'});
  271. converse.connection._dataRecv(test_utils.createRequest(stanza));
  272. expect(view.join).toHaveBeenCalled();
  273. // The user has just entered the room (because join was called)
  274. // and receives their own presence from the server.
  275. // See example 24:
  276. // http://xmpp.org/extensions/xep-0045.html#enter-pres
  277. var presence = $pres({
  278. to:'dummy@localhost/resource',
  279. from:'lounge@localhost/thirdwitch',
  280. id:'DC352437-C019-40EC-B590-AF29E879AF97'
  281. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  282. .c('item').attrs({
  283. affiliation: 'member',
  284. jid: 'dummy@localhost/resource',
  285. role: 'occupant'
  286. }).up()
  287. .c('status').attrs({code:'110'}).up()
  288. .c('status').attrs({code:'210'}).nodeTree;
  289. converse.connection._dataRecv(test_utils.createRequest(presence));
  290. var info_text = view.$el.find('.chat-content .chat-info').text();
  291. expect(info_text).toBe('Your nickname has been automatically set to: thirdwitch');
  292. });
  293. it("allows the user to invite their roster contacts to enter the chat room", function () {
  294. test_utils.openChatRoom('lounge', 'localhost', 'dummy');
  295. spyOn(converse, 'emit');
  296. spyOn(window, 'prompt').andCallFake(function () {
  297. return null;
  298. });
  299. var $input;
  300. var view = this.chatboxviews.get('lounge@localhost');
  301. view.$el.find('.chat-area').remove();
  302. view.renderChatArea(); // Will init the widget
  303. test_utils.createContacts('current'); // We need roster contacts, so that we have someone to invite
  304. $input = view.$el.find('input.invited-contact.tt-input');
  305. var $hint = view.$el.find('input.invited-contact.tt-hint');
  306. runs (function () {
  307. expect($input.length).toBe(1);
  308. expect($input.attr('placeholder')).toBe('Invite');
  309. $input.val("Felix");
  310. $input.trigger('input');
  311. });
  312. waits(350); // Needed, due to debounce
  313. runs (function () {
  314. expect($input.val()).toBe('Felix');
  315. expect($hint.val()).toBe('Felix Amsel');
  316. var $sugg = view.$el.find('[data-jid="felix.amsel@localhost"]');
  317. expect($sugg.length).toBe(1);
  318. $sugg.trigger('click');
  319. expect(window.prompt).toHaveBeenCalled();
  320. });
  321. }.bind(converse));
  322. it("can be joined automatically, based upon a received invite", function () {
  323. test_utils.openChatRoom('lounge', 'localhost', 'dummy');
  324. spyOn(window, 'confirm').andCallFake(function () {
  325. return true;
  326. });
  327. test_utils.createContacts('current'); // We need roster contacts, who can invite us
  328. var view = this.chatboxviews.get('lounge@localhost');
  329. view.close();
  330. var name = mock.cur_names[0];
  331. var from_jid = name.replace(/ /g,'.').toLowerCase() + '@localhost';
  332. var room_jid = 'lounge@localhost';
  333. var reason = "Please join this chat room";
  334. var message = $(
  335. "<message from='"+from_jid+"' to='"+converse.bare_jid+"'>" +
  336. "<x xmlns='jabber:x:conference'" +
  337. "jid='"+room_jid+"'" +
  338. "reason='"+reason+"'/>"+
  339. "</message>"
  340. )[0];
  341. expect(converse.chatboxes.models.length).toBe(1);
  342. expect(converse.chatboxes.models[0].id).toBe("controlbox");
  343. converse.onDirectMUCInvitation(message);
  344. expect(window.confirm).toHaveBeenCalledWith(
  345. name + ' has invited you to join a chat room: '+ room_jid +
  346. ', and left the following reason: "'+reason+'"');
  347. expect(converse.chatboxes.models.length).toBe(2);
  348. expect(converse.chatboxes.models[0].id).toBe('controlbox');
  349. expect(converse.chatboxes.models[1].id).toBe(room_jid);
  350. }.bind(converse));
  351. it("shows received groupchat messages", function () {
  352. test_utils.openChatRoom('lounge', 'localhost', 'dummy');
  353. spyOn(converse, 'emit');
  354. var view = this.chatboxviews.get('lounge@localhost');
  355. if (!view.$el.find('.chat-area').length) { view.renderChatArea(); }
  356. var nick = mock.chatroom_names[0];
  357. var text = 'This is a received message';
  358. var message = $msg({
  359. from: 'lounge@localhost/'+nick,
  360. id: '1',
  361. to: 'dummy@localhost',
  362. type: 'groupchat'
  363. }).c('body').t(text);
  364. view.onChatRoomMessage(message.nodeTree);
  365. var $chat_content = view.$el.find('.chat-content');
  366. expect($chat_content.find('.chat-message').length).toBe(1);
  367. expect($chat_content.find('.chat-msg-content').text()).toBe(text);
  368. expect(converse.emit).toHaveBeenCalledWith('message', message.nodeTree);
  369. }.bind(converse));
  370. it("shows sent groupchat messages", function () {
  371. test_utils.openAndEnterChatRoom('lounge', 'localhost', 'dummy');
  372. spyOn(converse, 'emit');
  373. var view = converse.chatboxviews.get('lounge@localhost');
  374. if (!view.$el.find('.chat-area').length) { view.renderChatArea(); }
  375. var text = 'This is a sent message';
  376. view.$el.find('.chat-textarea').text(text);
  377. view.$el.find('textarea.chat-textarea').trigger($.Event('keypress', {keyCode: 13}));
  378. expect(converse.emit).toHaveBeenCalledWith('messageSend', text);
  379. var $chat_content = view.$el.find('.chat-content');
  380. expect($chat_content.find('.chat-message').length).toBe(1);
  381. // Let's check that if we receive the same message again, it's
  382. // not shown.
  383. var message = $msg({
  384. from: 'lounge@localhost/dummy',
  385. to: 'dummy@localhost.com',
  386. type: 'groupchat',
  387. id: view.model.messages.at(0).get('msgid')
  388. }).c('body').t(text);
  389. view.onChatRoomMessage(message.nodeTree);
  390. expect($chat_content.find('.chat-message').length).toBe(1);
  391. expect($chat_content.find('.chat-msg-content').last().text()).toBe(text);
  392. // We don't emit an event if it's our own message
  393. expect(converse.emit.callCount, 1);
  394. });
  395. it("will cause the chat area to be scrolled down only if it was at the bottom already", function () {
  396. var message = 'This message is received while the chat area is scrolled up';
  397. test_utils.openAndEnterChatRoom('lounge', 'localhost', 'dummy');
  398. var view = converse.chatboxviews.get('lounge@localhost');
  399. spyOn(view, 'scrollDown').andCallThrough();
  400. runs(function () {
  401. /* Create enough messages so that there's a
  402. * scrollbar.
  403. */
  404. for (var i=0; i<20; i++) {
  405. converse.chatboxes.onMessage(
  406. $msg({
  407. from: 'lounge@localhost/someone',
  408. to: 'dummy@localhost.com',
  409. type: 'groupchat',
  410. id: (new Date()).getTime(),
  411. }).c('body').t('Message: '+i).tree());
  412. }
  413. });
  414. waits(50);
  415. runs(function () {
  416. view.$content.scrollTop(0);
  417. });
  418. waits(250);
  419. runs(function () {
  420. expect(view.model.get('scrolled')).toBeTruthy();
  421. converse.chatboxes.onMessage(
  422. $msg({
  423. from: 'lounge@localhost/someone',
  424. to: 'dummy@localhost.com',
  425. type: 'groupchat',
  426. id: (new Date()).getTime(),
  427. }).c('body').t(message).tree());
  428. });
  429. waits(150);
  430. runs(function () {
  431. // Now check that the message appears inside the chatbox in the DOM
  432. var $chat_content = view.$el.find('.chat-content');
  433. var msg_txt = $chat_content.find('.chat-message:last').find('.chat-msg-content').text();
  434. expect(msg_txt).toEqual(message);
  435. expect(view.$content.scrollTop()).toBe(0);
  436. });
  437. });
  438. it("shows received chatroom subject messages", function () {
  439. test_utils.openAndEnterChatRoom('jdev', 'conference.jabber.org', 'jc');
  440. var text = 'Jabber/XMPP Development | RFCs and Extensions: http://xmpp.org/ | Protocol and XSF discussions: xsf@muc.xmpp.org';
  441. var stanza = Strophe.xmlHtmlNode(
  442. '<message xmlns="jabber:client" to="jc@opkode.com/converse.js-60429116" type="groupchat" from="jdev@conference.jabber.org/ralphm">'+
  443. ' <subject>'+text+'</subject>'+
  444. ' <delay xmlns="urn:xmpp:delay" stamp="2014-02-04T09:35:39Z" from="jdev@conference.jabber.org"/>'+
  445. ' <x xmlns="jabber:x:delay" stamp="20140204T09:35:39" from="jdev@conference.jabber.org"/>'+
  446. '</message>').firstChild;
  447. converse.connection._dataRecv(test_utils.createRequest(stanza));
  448. var view = converse.chatboxviews.get('jdev@conference.jabber.org');
  449. var $chat_content = view.$el.find('.chat-content');
  450. expect($chat_content.find('.chat-info').length).toBe(1);
  451. expect($chat_content.find('.chat-info').text()).toBe('Topic set by ralphm to: '+text);
  452. });
  453. it("informs users if their nicknames has been changed.", function () {
  454. /* The service then sends two presence stanzas to the full JID
  455. * of each occupant (including the occupant who is changing his
  456. * or her room nickname), one of type "unavailable" for the old
  457. * nickname and one indicating availability for the new
  458. * nickname.
  459. *
  460. * See: http://xmpp.org/extensions/xep-0045.html#changenick
  461. *
  462. * <presence
  463. * from='coven@localhost/thirdwitch'
  464. * id='DC352437-C019-40EC-B590-AF29E879AF98'
  465. * to='hag66@shakespeare.lit/pda'
  466. * type='unavailable'>
  467. * <x xmlns='http://jabber.org/protocol/muc#user'>
  468. * <item affiliation='member'
  469. * jid='hag66@shakespeare.lit/pda'
  470. * nick='oldhag'
  471. * role='occupant'/>
  472. * <status code='303'/>
  473. * <status code='110'/>
  474. * </x>
  475. * </presence>
  476. *
  477. * <presence
  478. * from='coven@localhost/oldhag'
  479. * id='5B4F27A4-25ED-43F7-A699-382C6B4AFC67'
  480. * to='hag66@shakespeare.lit/pda'>
  481. * <x xmlns='http://jabber.org/protocol/muc#user'>
  482. * <item affiliation='member'
  483. * jid='hag66@shakespeare.lit/pda'
  484. * role='occupant'/>
  485. * <status code='110'/>
  486. * </x>
  487. * </presence>
  488. */
  489. var __ = utils.__.bind(converse);
  490. test_utils.openAndEnterChatRoom('lounge', 'localhost', 'oldnick');
  491. var view = converse.chatboxviews.get('lounge@localhost');
  492. var $chat_content = view.$el.find('.chat-content');
  493. spyOn(view, 'onChatRoomPresence').andCallThrough();
  494. // The user has just entered the room and receives their own
  495. // presence from the server.
  496. // See example 24:
  497. // http://xmpp.org/extensions/xep-0045.html#enter-pres
  498. var presence = $pres({
  499. to:'dummy@localhost/pda',
  500. from:'lounge@localhost/oldnick',
  501. id:'DC352437-C019-40EC-B590-AF29E879AF97'
  502. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  503. .c('item').attrs({
  504. affiliation: 'member',
  505. jid: 'dummy@localhost/pda',
  506. role: 'occupant'
  507. }).up()
  508. .c('status').attrs({code:'110'}).up()
  509. .c('status').attrs({code:'210'}).nodeTree;
  510. converse.connection._dataRecv(test_utils.createRequest(presence));
  511. expect(view.onChatRoomPresence).toHaveBeenCalled();
  512. var $occupants = view.$('.occupant-list');
  513. expect($occupants.children().length).toBe(1);
  514. expect($occupants.children().first(0).text()).toBe("oldnick");
  515. expect($chat_content.find('div.chat-info').length).toBe(1);
  516. expect($chat_content.find('div.chat-info').html()).toBe(__(view.newNicknameMessages["210"], "oldnick"));
  517. presence = $pres().attrs({
  518. from:'lounge@localhost/oldnick',
  519. id:'DC352437-C019-40EC-B590-AF29E879AF98',
  520. to:'dummy@localhost/pda',
  521. type:'unavailable'
  522. })
  523. .c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  524. .c('item').attrs({
  525. affiliation: 'member',
  526. jid: 'dummy@localhost/pda',
  527. nick: 'newnick',
  528. role: 'occupant'
  529. }).up()
  530. .c('status').attrs({code:'303'}).up()
  531. .c('status').attrs({code:'110'}).nodeTree;
  532. converse.connection._dataRecv(test_utils.createRequest(presence));
  533. expect(view.onChatRoomPresence).toHaveBeenCalled();
  534. expect($chat_content.find('div.chat-info').length).toBe(2);
  535. expect($chat_content.find('div.chat-info').last().html()).toBe(__(view.newNicknameMessages["303"], "newnick"));
  536. $occupants = view.$('.occupant-list');
  537. expect($occupants.children().length).toBe(0);
  538. presence = $pres().attrs({
  539. from:'lounge@localhost/newnick',
  540. id:'5B4F27A4-25ED-43F7-A699-382C6B4AFC67',
  541. to:'dummy@localhost/pda'
  542. })
  543. .c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  544. .c('item').attrs({
  545. affiliation: 'member',
  546. jid: 'dummy@localhost/pda',
  547. role: 'occupant'
  548. }).up()
  549. .c('status').attrs({code:'110'}).nodeTree;
  550. converse.connection._dataRecv(test_utils.createRequest(presence));
  551. expect(view.onChatRoomPresence).toHaveBeenCalled();
  552. expect($chat_content.find('div.chat-info').length).toBe(2);
  553. expect($chat_content.find('div.chat-info').last().html()).toBe(__(view.newNicknameMessages["303"], "newnick"));
  554. $occupants = view.$('.occupant-list');
  555. expect($occupants.children().length).toBe(1);
  556. expect($occupants.children().first(0).text()).toBe("newnick");
  557. });
  558. it("informs users if they have been kicked out of the chat room", function () {
  559. /* <presence
  560. * from='harfleur@chat.shakespeare.lit/pistol'
  561. * to='pistol@shakespeare.lit/harfleur'
  562. * type='unavailable'>
  563. * <x xmlns='http://jabber.org/protocol/muc#user'>
  564. * <item affiliation='none' role='none'>
  565. * <actor nick='Fluellen'/>
  566. * <reason>Avaunt, you cullion!</reason>
  567. * </item>
  568. * <status code='307'/>
  569. * </x>
  570. * </presence>
  571. */
  572. test_utils.openAndEnterChatRoom('lounge', 'localhost', 'dummy');
  573. var presence = $pres().attrs({
  574. from:'lounge@localhost/dummy',
  575. to:'dummy@localhost/pda',
  576. type:'unavailable'
  577. })
  578. .c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  579. .c('item').attrs({
  580. affiliation: 'none',
  581. jid: 'dummy@localhost/pda',
  582. role: 'none'
  583. })
  584. .c('actor').attrs({nick: 'Fluellen'}).up()
  585. .c('reason').t('Avaunt, you cullion!').up()
  586. .up()
  587. .c('status').attrs({code:'307'}).nodeTree;
  588. var view = this.chatboxviews.get('lounge@localhost');
  589. view.onChatRoomPresence(presence);
  590. expect(view.$('.chat-area').is(':visible')).toBeFalsy();
  591. expect(view.$('.occupants').is(':visible')).toBeFalsy();
  592. var $chat_body = view.$('.chatroom-body');
  593. expect($chat_body.html().trim().indexOf('<p>You have been kicked from this room</p><p>The reason given is: "Avaunt, you cullion!"</p>')).not.toBe(-1);
  594. }.bind(converse));
  595. it("can be saved to, and retrieved from, browserStorage", function () {
  596. test_utils.openChatRoom('lounge', 'localhost', 'dummy');
  597. // We instantiate a new ChatBoxes collection, which by default
  598. // will be empty.
  599. test_utils.openControlBox();
  600. var newchatboxes = new this.ChatBoxes();
  601. expect(newchatboxes.length).toEqual(0);
  602. // The chatboxes will then be fetched from browserStorage inside the
  603. // onConnected method
  604. newchatboxes.onConnected();
  605. expect(newchatboxes.length).toEqual(2);
  606. // Check that the chatrooms retrieved from browserStorage
  607. // have the same attributes values as the original ones.
  608. var attrs = ['id', 'box_id', 'visible'];
  609. var new_attrs, old_attrs;
  610. for (var i=0; i<attrs.length; i++) {
  611. new_attrs = _.pluck(_.pluck(newchatboxes.models, 'attributes'), attrs[i]);
  612. old_attrs = _.pluck(_.pluck(this.chatboxes.models, 'attributes'), attrs[i]);
  613. // FIXME: should have have to sort here? Order must
  614. // probably be the same...
  615. // This should be fixed once the controlbox always opens
  616. // only on the right.
  617. expect(_.isEqual(new_attrs.sort(), old_attrs.sort())).toEqual(true);
  618. }
  619. this.rosterview.render();
  620. }.bind(converse));
  621. it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'", function () {
  622. test_utils.openChatRoom('lounge', 'localhost', 'dummy');
  623. var view = this.chatboxviews.get('lounge@localhost'),
  624. trimmed_chatboxes = this.minimized_chats;
  625. spyOn(view, 'minimize').andCallThrough();
  626. spyOn(view, 'maximize').andCallThrough();
  627. spyOn(converse, 'emit');
  628. view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
  629. runs(function () {
  630. view.$el.find('.toggle-chatbox-button').click();
  631. });
  632. waits(50);
  633. runs(function () {
  634. expect(view.minimize).toHaveBeenCalled();
  635. expect(converse.emit).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object));
  636. expect(converse.emit.callCount, 2);
  637. expect(view.$el.is(':visible')).toBeFalsy();
  638. expect(view.model.get('minimized')).toBeTruthy();
  639. expect(view.minimize).toHaveBeenCalled();
  640. var trimmedview = trimmed_chatboxes.get(view.model.get('id'));
  641. trimmedview.$("a.restore-chat").click();
  642. });
  643. waits(250);
  644. runs(function () {
  645. expect(view.maximize).toHaveBeenCalled();
  646. expect(converse.emit).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
  647. expect(view.$el.is(':visible')).toBeTruthy();
  648. expect(view.model.get('minimized')).toBeFalsy();
  649. expect(converse.emit.callCount, 3);
  650. });
  651. }.bind(converse));
  652. it("can be closed again by clicking a DOM element with class 'close-chatbox-button'", function () {
  653. test_utils.openChatRoom('lounge', 'localhost', 'dummy');
  654. var view = this.chatboxviews.get('lounge@localhost');
  655. spyOn(view, 'close').andCallThrough();
  656. spyOn(converse, 'emit');
  657. spyOn(view, 'leave');
  658. view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
  659. runs(function () {
  660. view.$el.find('.close-chatbox-button').click();
  661. });
  662. waits(50);
  663. runs(function () {
  664. expect(view.close).toHaveBeenCalled();
  665. expect(view.leave).toHaveBeenCalled();
  666. expect(this.emit).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object));
  667. }.bind(converse));
  668. }.bind(converse));
  669. }.bind(converse));
  670. describe("Each chat room can take special commands", function () {
  671. beforeEach(function () {
  672. runs(function () {
  673. test_utils.closeAllChatBoxes();
  674. test_utils.clearBrowserStorage();
  675. });
  676. });
  677. it("to clear messages", function () {
  678. test_utils.openChatRoom('lounge', 'localhost', 'dummy');
  679. var view = converse.chatboxviews.get('lounge@localhost');
  680. spyOn(view, 'onMessageSubmitted').andCallThrough();
  681. spyOn(view, 'clearChatRoomMessages');
  682. view.$el.find('.chat-textarea').text('/clear');
  683. view.$el.find('textarea.chat-textarea').trigger($.Event('keypress', {keyCode: 13}));
  684. expect(view.onMessageSubmitted).toHaveBeenCalled();
  685. expect(view.clearChatRoomMessages).toHaveBeenCalled();
  686. });
  687. it("to ban a user", function () {
  688. test_utils.openChatRoom('lounge', 'localhost', 'dummy');
  689. var view = converse.chatboxviews.get('lounge@localhost');
  690. spyOn(view, 'onMessageSubmitted').andCallThrough();
  691. spyOn(view, 'setAffiliation').andCallThrough();
  692. spyOn(view, 'showStatusNotification').andCallThrough();
  693. spyOn(view, 'validateRoleChangeCommand').andCallThrough();
  694. view.$el.find('.chat-textarea').text('/ban');
  695. view.$el.find('textarea.chat-textarea').trigger($.Event('keypress', {keyCode: 13}));
  696. expect(view.onMessageSubmitted).toHaveBeenCalled();
  697. expect(view.validateRoleChangeCommand).toHaveBeenCalled();
  698. expect(view.showStatusNotification).toHaveBeenCalledWith(
  699. "Error: the \"ban\" command takes two arguments, the user's nickname and optionally a reason.",
  700. true
  701. );
  702. expect(view.setAffiliation).not.toHaveBeenCalled();
  703. // Call now with the correct amount of arguments.
  704. // XXX: Calling onMessageSubmitted directly, trying
  705. // again via triggering Event doesn't work for some weird
  706. // reason.
  707. view.onMessageSubmitted('/ban jid This is the reason');
  708. expect(view.validateRoleChangeCommand.callCount).toBe(2);
  709. expect(view.showStatusNotification.callCount).toBe(1);
  710. expect(view.setAffiliation).toHaveBeenCalled();
  711. });
  712. }.bind(converse));
  713. describe("When attempting to enter a chatroom", function () {
  714. beforeEach(function () {
  715. var roomspanel = this.chatboxviews.get('controlbox').roomspanel;
  716. var $input = roomspanel.$el.find('input.new-chatroom-name');
  717. var $nick = roomspanel.$el.find('input.new-chatroom-nick');
  718. var $server = roomspanel.$el.find('input.new-chatroom-server');
  719. $input.val('problematic');
  720. $nick.val('dummy');
  721. $server.val('muc.localhost');
  722. roomspanel.$el.find('form').submit();
  723. }.bind(converse));
  724. afterEach(function () {
  725. var view = this.chatboxviews.get('problematic@muc.localhost');
  726. view.close();
  727. }.bind(converse));
  728. it("will show an error message if the room requires a password", function () {
  729. var presence = $pres().attrs({
  730. from:'lounge@localhost/thirdwitch',
  731. id:'n13mt3l',
  732. to:'dummy@localhost/pda',
  733. type:'error'})
  734. .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
  735. .c('error').attrs({by:'lounge@localhost', type:'auth'})
  736. .c('not-authorized').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
  737. var view = this.chatboxviews.get('problematic@muc.localhost');
  738. spyOn(view, 'renderPasswordForm').andCallThrough();
  739. runs(function () {
  740. view.onChatRoomPresence(presence);
  741. });
  742. waits(250);
  743. runs(function () {
  744. var $chat_body = view.$el.find('.chatroom-body');
  745. expect(view.renderPasswordForm).toHaveBeenCalled();
  746. expect($chat_body.find('form.chatroom-form').length).toBe(1);
  747. expect($chat_body.find('legend').text()).toBe('This chatroom requires a password');
  748. });
  749. }.bind(converse));
  750. it("will show an error message if the room is members-only and the user not included", function () {
  751. var presence = $pres().attrs({
  752. from:'lounge@localhost/thirdwitch',
  753. id:'n13mt3l',
  754. to:'dummy@localhost/pda',
  755. type:'error'})
  756. .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
  757. .c('error').attrs({by:'lounge@localhost', type:'auth'})
  758. .c('registration-required').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
  759. var view = converse.chatboxviews.get('problematic@muc.localhost');
  760. spyOn(view, 'showErrorMessage').andCallThrough();
  761. view.onChatRoomPresence(presence);
  762. expect(view.$el.find('.chatroom-body p:last').text()).toBe('You are not on the member list of this room');
  763. });
  764. it("will show an error message if the user has been banned", function () {
  765. var presence = $pres().attrs({
  766. from:'lounge@localhost/thirdwitch',
  767. id:'n13mt3l',
  768. to:'dummy@localhost/pda',
  769. type:'error'})
  770. .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
  771. .c('error').attrs({by:'lounge@localhost', type:'auth'})
  772. .c('forbidden').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
  773. var view = converse.chatboxviews.get('problematic@muc.localhost');
  774. spyOn(view, 'showErrorMessage').andCallThrough();
  775. view.onChatRoomPresence(presence);
  776. expect(view.$el.find('.chatroom-body p:last').text()).toBe('You have been banned from this room');
  777. });
  778. it("will render a nickname form if a nickname conflict happens and muc_nickname_from_jid=false", function () {
  779. var presence = $pres().attrs({
  780. from:'lounge@localhost/thirdwitch',
  781. id:'n13mt3l',
  782. to:'dummy@localhost/pda',
  783. type:'error'})
  784. .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
  785. .c('error').attrs({by:'lounge@localhost', type:'cancel'})
  786. .c('conflict').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
  787. var view = converse.chatboxviews.get('problematic@muc.localhost');
  788. spyOn(view, 'showErrorMessage').andCallThrough();
  789. view.onChatRoomPresence(presence);
  790. expect(view.$el.find('.chatroom-body form.chatroom-form label:first').text()).toBe('Please choose your nickname');
  791. });
  792. it("will automatically choose a new nickname if a nickname conflict happens and muc_nickname_from_jid=true", function () {
  793. /*
  794. <presence
  795. from='coven@chat.shakespeare.lit/thirdwitch'
  796. id='n13mt3l'
  797. to='hag66@shakespeare.lit/pda'
  798. type='error'>
  799. <x xmlns='http://jabber.org/protocol/muc'/>
  800. <error by='coven@chat.shakespeare.lit' type='cancel'>
  801. <conflict xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
  802. </error>
  803. </presence>
  804. */
  805. converse.muc_nickname_from_jid = true;
  806. var attrs = {
  807. from:'lounge@localhost/dummy',
  808. id:'n13mt3l',
  809. to:'dummy@localhost/pda',
  810. type:'error'
  811. };
  812. var presence = $pres().attrs(attrs)
  813. .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
  814. .c('error').attrs({by:'lounge@localhost', type:'cancel'})
  815. .c('conflict').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
  816. var view = converse.chatboxviews.get('problematic@muc.localhost');
  817. spyOn(view, 'showErrorMessage').andCallThrough();
  818. spyOn(view, 'join').andCallThrough();
  819. // Simulate repeatedly that there's already someone in the room
  820. // with that nickname
  821. view.onChatRoomPresence(presence);
  822. expect(view.join).toHaveBeenCalledWith('dummy-2');
  823. attrs.from = 'lounge@localhost/dummy-2';
  824. presence = $pres().attrs(attrs)
  825. .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
  826. .c('error').attrs({by:'lounge@localhost', type:'cancel'})
  827. .c('conflict').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
  828. view.onChatRoomPresence(presence);
  829. expect(view.join).toHaveBeenCalledWith('dummy-3');
  830. attrs.from = 'lounge@localhost/dummy-3';
  831. presence = $pres().attrs(attrs)
  832. .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
  833. .c('error').attrs({by:'lounge@localhost', type:'cancel'})
  834. .c('conflict').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
  835. view.onChatRoomPresence(presence);
  836. expect(view.join).toHaveBeenCalledWith('dummy-4');
  837. });
  838. it("will show an error message if the user is not allowed to have created the room", function () {
  839. var presence = $pres().attrs({
  840. from:'lounge@localhost/thirdwitch',
  841. id:'n13mt3l',
  842. to:'dummy@localhost/pda',
  843. type:'error'})
  844. .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
  845. .c('error').attrs({by:'lounge@localhost', type:'cancel'})
  846. .c('not-allowed').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
  847. var view = this.chatboxviews.get('problematic@muc.localhost');
  848. spyOn(view, 'showErrorMessage').andCallThrough();
  849. view.onChatRoomPresence(presence);
  850. expect(view.$el.find('.chatroom-body p:last').text()).toBe('You are not allowed to create new rooms');
  851. }.bind(converse));
  852. it("will show an error message if the user's nickname doesn't conform to room policy", function () {
  853. var presence = $pres().attrs({
  854. from:'lounge@localhost/thirdwitch',
  855. id:'n13mt3l',
  856. to:'dummy@localhost/pda',
  857. type:'error'})
  858. .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
  859. .c('error').attrs({by:'lounge@localhost', type:'cancel'})
  860. .c('not-acceptable').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
  861. var view = converse.chatboxviews.get('problematic@muc.localhost');
  862. spyOn(view, 'showErrorMessage').andCallThrough();
  863. view.onChatRoomPresence(presence);
  864. expect(view.$el.find('.chatroom-body p:last').text()).toBe("Your nickname doesn't conform to this room's policies");
  865. });
  866. it("will show an error message if the room doesn't yet exist", function () {
  867. var presence = $pres().attrs({
  868. from:'lounge@localhost/thirdwitch',
  869. id:'n13mt3l',
  870. to:'dummy@localhost/pda',
  871. type:'error'})
  872. .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
  873. .c('error').attrs({by:'lounge@localhost', type:'cancel'})
  874. .c('item-not-found').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
  875. var view = converse.chatboxviews.get('problematic@muc.localhost');
  876. spyOn(view, 'showErrorMessage').andCallThrough();
  877. view.onChatRoomPresence(presence);
  878. expect(view.$el.find('.chatroom-body p:last').text()).toBe("This room does not (yet) exist");
  879. });
  880. it("will show an error message if the room has reached its maximum number of occupants", function () {
  881. var presence = $pres().attrs({
  882. from:'lounge@localhost/thirdwitch',
  883. id:'n13mt3l',
  884. to:'dummy@localhost/pda',
  885. type:'error'})
  886. .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
  887. .c('error').attrs({by:'lounge@localhost', type:'cancel'})
  888. .c('service-unavailable').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
  889. var view = converse.chatboxviews.get('problematic@muc.localhost');
  890. spyOn(view, 'showErrorMessage').andCallThrough();
  891. view.onChatRoomPresence(presence);
  892. expect(view.$el.find('.chatroom-body p:last').text()).toBe("This room has reached its maximum number of occupants");
  893. });
  894. }.bind(converse));
  895. }.bind(converse, mock, test_utils));
  896. }));