muc_messages.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. /*global mock, converse */
  2. const { Promise, Strophe, $msg, $pres, sizzle } = converse.env;
  3. const u = converse.env.utils;
  4. const original_timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
  5. describe("A Groupchat Message", function () {
  6. beforeEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000));
  7. afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
  8. describe("which is succeeded by an error message", function () {
  9. it("will have the error displayed below it",
  10. mock.initConverse([], {}, async function (done, _converse) {
  11. const muc_jid = 'lounge@montague.lit';
  12. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  13. const view = _converse.api.chatviews.get(muc_jid);
  14. const textarea = view.querySelector('textarea.chat-textarea');
  15. textarea.value = 'hello world'
  16. const enter_event = {
  17. 'target': textarea,
  18. 'preventDefault': function preventDefault () {},
  19. 'stopPropagation': function stopPropagation () {},
  20. 'keyCode': 13 // Enter
  21. }
  22. view.onKeyDown(enter_event);
  23. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  24. const msg = view.model.messages.at(0);
  25. const err_msg_text = "Message rejected because you're sending messages too quickly";
  26. const error = u.toStanza(`
  27. <message xmlns="jabber:client" id="${msg.get('msgid')}" from="${muc_jid}" to="${_converse.jid}" type="error">
  28. <error type="wait">
  29. <policy-violation xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
  30. <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">${err_msg_text}</text>
  31. </error>
  32. <body>hello world</body>
  33. </message>
  34. `);
  35. _converse.connection._dataRecv(mock.createRequest(error));
  36. expect(await u.waitUntil(() => view.querySelector('.chat-msg__error')?.textContent?.trim())).toBe(err_msg_text);
  37. expect(view.model.messages.length).toBe(1);
  38. const message = view.model.messages.at(0);
  39. expect(message.get('received')).toBeUndefined();
  40. expect(message.get('body')).toBe('hello world');
  41. expect(message.get('error_text')).toBe(err_msg_text);
  42. expect(message.get('editable')).toBe(false);
  43. done();
  44. }));
  45. });
  46. describe("an info message", function () {
  47. it("is not rendered as a followup message",
  48. mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
  49. const muc_jid = 'lounge@montague.lit';
  50. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  51. const view = _converse.api.chatviews.get(muc_jid);
  52. let presence = u.toStanza(`
  53. <presence xmlns="jabber:client" to="${_converse.jid}" from="${muc_jid}/romeo">
  54. <x xmlns="http://jabber.org/protocol/muc#user">
  55. <status code="201"/>
  56. <item role="moderator" affiliation="owner" jid="${_converse.jid}"/>
  57. <status code="110"/>
  58. </x>
  59. </presence>
  60. `);
  61. _converse.connection._dataRecv(mock.createRequest(presence));
  62. await u.waitUntil(() => view.querySelectorAll('.chat-info').length === 1);
  63. presence = u.toStanza(`
  64. <presence xmlns="jabber:client" to="${_converse.jid}" from="${muc_jid}/romeo1">
  65. <x xmlns="http://jabber.org/protocol/muc#user">
  66. <status code="210"/>
  67. <item role="moderator" affiliation="owner" jid="${_converse.jid}"/>
  68. <status code="110"/>
  69. </x>
  70. </presence>
  71. `);
  72. _converse.connection._dataRecv(mock.createRequest(presence));
  73. await u.waitUntil(() => view.querySelectorAll('.chat-info').length === 2);
  74. const messages = view.querySelectorAll('.chat-info');
  75. expect(u.hasClass('chat-msg--followup', messages[0])).toBe(false);
  76. expect(u.hasClass('chat-msg--followup', messages[1])).toBe(false);
  77. done();
  78. }));
  79. it("is not shown if its a duplicate",
  80. mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
  81. const muc_jid = 'lounge@montague.lit';
  82. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  83. const view = _converse.api.chatviews.get(muc_jid);
  84. const presence = u.toStanza(`
  85. <presence xmlns="jabber:client" to="${_converse.jid}" from="${muc_jid}/romeo">
  86. <x xmlns="http://jabber.org/protocol/muc#user">
  87. <status code="201"/>
  88. <item role="moderator" affiliation="owner" jid="${_converse.jid}"/>
  89. <status code="110"/>
  90. </x>
  91. </presence>
  92. `);
  93. // XXX: We wait for createInfoMessages to complete, if we don't
  94. // we still get two info messages due to messages
  95. // created from presences not being queued and run
  96. // sequentially (i.e. by waiting for promises to resolve)
  97. // like we do with message stanzas.
  98. spyOn(view.model, 'createInfoMessages').and.callThrough();
  99. _converse.connection._dataRecv(mock.createRequest(presence));
  100. await u.waitUntil(() => view.model.createInfoMessages.calls.count());
  101. await u.waitUntil(() => view.querySelectorAll('.chat-info').length === 1);
  102. _converse.connection._dataRecv(mock.createRequest(presence));
  103. await u.waitUntil(() => view.model.createInfoMessages.calls.count() === 2);
  104. expect(view.querySelectorAll('.chat-info').length).toBe(1);
  105. done();
  106. }));
  107. });
  108. it("is rejected if it's an unencapsulated forwarded message",
  109. mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
  110. const muc_jid = 'lounge@montague.lit';
  111. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  112. const impersonated_jid = `${muc_jid}/alice`;
  113. const received_stanza = u.toStanza(`
  114. <message to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  115. <forwarded xmlns='urn:xmpp:forward:0'>
  116. <delay xmlns='urn:xmpp:delay' stamp='2019-07-10T23:08:25Z'/>
  117. <message from='${impersonated_jid}'
  118. id='0202197'
  119. to='${_converse.bare_jid}'
  120. type='groupchat'
  121. xmlns='jabber:client'>
  122. <body>Yet I should kill thee with much cherishing.</body>
  123. </message>
  124. </forwarded>
  125. </message>
  126. `);
  127. const view = _converse.api.chatviews.get(muc_jid);
  128. spyOn(view.model, 'onMessage').and.callThrough();
  129. spyOn(converse.env.log, 'error');
  130. _converse.connection._dataRecv(mock.createRequest(received_stanza));
  131. await u.waitUntil(() => view.model.onMessage.calls.count() === 1);
  132. expect(converse.env.log.error).toHaveBeenCalledWith(
  133. `Ignoring unencapsulated forwarded message from ${muc_jid}/mallory`
  134. );
  135. expect(view.querySelectorAll('.chat-msg').length).toBe(0);
  136. expect(view.model.messages.length).toBe(0);
  137. done();
  138. }));
  139. it("can contain a chat state notification and will still be shown",
  140. mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
  141. const muc_jid = 'lounge@montague.lit';
  142. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  143. const view = _converse.api.chatviews.get(muc_jid);
  144. if (!view.querySelectorAll('.chat-area').length) { view.renderChatArea(); }
  145. const message = 'romeo: Your attention is required';
  146. const nick = mock.chatroom_names[0],
  147. msg = $msg({
  148. from: 'lounge@montague.lit/'+nick,
  149. id: u.getUniqueId(),
  150. to: 'romeo@montague.lit',
  151. type: 'groupchat'
  152. }).c('body').t(message)
  153. .c('active', {'xmlns': "http://jabber.org/protocol/chatstates"})
  154. .tree();
  155. await view.model.handleMessageStanza(msg);
  156. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  157. expect(view.querySelector('.chat-msg')).not.toBe(null);
  158. done();
  159. }));
  160. it("can not be expected to have a unique id attribute",
  161. mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
  162. const muc_jid = 'lounge@montague.lit';
  163. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  164. const view = _converse.api.chatviews.get(muc_jid);
  165. if (!view.querySelectorAll('.chat-area').length) { view.renderChatArea(); }
  166. const id = u.getUniqueId();
  167. let msg = $msg({
  168. from: 'lounge@montague.lit/some1',
  169. id: id,
  170. to: 'romeo@montague.lit',
  171. type: 'groupchat'
  172. }).c('body').t('First message').tree();
  173. await view.model.handleMessageStanza(msg);
  174. await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 1);
  175. msg = $msg({
  176. from: 'lounge@montague.lit/some2',
  177. id: id,
  178. to: 'romeo@montague.lit',
  179. type: 'groupchat'
  180. }).c('body').t('Another message').tree();
  181. await view.model.handleMessageStanza(msg);
  182. await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 2);
  183. expect(view.model.messages.length).toBe(2);
  184. done();
  185. }));
  186. it("is ignored if it has the same archive-id of an already received one",
  187. mock.initConverse([], {}, async function (done, _converse) {
  188. const muc_jid = 'room@muc.example.com';
  189. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  190. const view = _converse.api.chatviews.get(muc_jid);
  191. spyOn(view.model, 'getDuplicateMessage').and.callThrough();
  192. let stanza = u.toStanza(`
  193. <message xmlns="jabber:client"
  194. from="room@muc.example.com/some1"
  195. to="${_converse.connection.jid}"
  196. type="groupchat">
  197. <body>Typical body text</body>
  198. <stanza-id xmlns="urn:xmpp:sid:0"
  199. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  200. by="room@muc.example.com"/>
  201. </message>`);
  202. _converse.connection._dataRecv(mock.createRequest(stanza));
  203. await u.waitUntil(() => view.model.messages.length === 1);
  204. await u.waitUntil(() => view.model.getDuplicateMessage.calls.count() === 1);
  205. let result = await view.model.getDuplicateMessage.calls.all()[0].returnValue;
  206. expect(result).toBe(undefined);
  207. stanza = u.toStanza(`
  208. <message xmlns="jabber:client"
  209. to="${_converse.connection.jid}"
  210. from="room@muc.example.com">
  211. <result xmlns="urn:xmpp:mam:2" queryid="82d9db27-6cf8-4787-8c2c-5a560263d823" id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad">
  212. <forwarded xmlns="urn:xmpp:forward:0">
  213. <delay xmlns="urn:xmpp:delay" stamp="2018-01-09T06:17:23Z"/>
  214. <message from="room@muc.example.com/some1" type="groupchat">
  215. <body>Typical body text</body>
  216. </message>
  217. </forwarded>
  218. </result>
  219. </message>`);
  220. spyOn(view.model, 'updateMessage');
  221. _converse.handleMAMResult(view.model, { 'messages': [stanza] });
  222. await u.waitUntil(() => view.model.getDuplicateMessage.calls.count() === 2);
  223. result = await view.model.getDuplicateMessage.calls.all()[1].returnValue;
  224. expect(result instanceof _converse.Message).toBe(true);
  225. expect(view.model.messages.length).toBe(1);
  226. await u.waitUntil(() => view.model.updateMessage.calls.count());
  227. done();
  228. }));
  229. it("is ignored if it has the same stanza-id of an already received one",
  230. mock.initConverse([], {}, async function (done, _converse) {
  231. const muc_jid = 'room@muc.example.com';
  232. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  233. const view = _converse.api.chatviews.get(muc_jid);
  234. spyOn(view.model, 'getStanzaIdQueryAttrs').and.callThrough();
  235. let stanza = u.toStanza(`
  236. <message xmlns="jabber:client"
  237. from="room@muc.example.com/some1"
  238. to="${_converse.connection.jid}"
  239. type="groupchat">
  240. <body>Typical body text</body>
  241. <stanza-id xmlns="urn:xmpp:sid:0"
  242. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  243. by="room@muc.example.com"/>
  244. </message>`);
  245. _converse.connection._dataRecv(mock.createRequest(stanza));
  246. await u.waitUntil(() => view.model.messages.length === 1);
  247. await u.waitUntil(() => view.model.getStanzaIdQueryAttrs.calls.count() === 1);
  248. let result = await view.model.getStanzaIdQueryAttrs.calls.all()[0].returnValue;
  249. expect(result instanceof Array).toBe(true);
  250. expect(result[0] instanceof Object).toBe(true);
  251. expect(result[0]['stanza_id room@muc.example.com']).toBe("5f3dbc5e-e1d3-4077-a492-693f3769c7ad");
  252. stanza = u.toStanza(`
  253. <message xmlns="jabber:client"
  254. from="room@muc.example.com/some1"
  255. to="${_converse.connection.jid}"
  256. type="groupchat">
  257. <body>Typical body text</body>
  258. <stanza-id xmlns="urn:xmpp:sid:0"
  259. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  260. by="room@muc.example.com"/>
  261. </message>`);
  262. spyOn(view.model, 'updateMessage');
  263. spyOn(view.model, 'getDuplicateMessage').and.callThrough();
  264. _converse.connection._dataRecv(mock.createRequest(stanza));
  265. await u.waitUntil(() => view.model.getDuplicateMessage.calls.count());
  266. result = await view.model.getDuplicateMessage.calls.all()[0].returnValue;
  267. expect(result instanceof _converse.Message).toBe(true);
  268. expect(view.model.messages.length).toBe(1);
  269. await u.waitUntil(() => view.model.updateMessage.calls.count());
  270. done();
  271. }));
  272. it("will be discarded if it's a malicious message meant to look like a carbon copy",
  273. mock.initConverse([], {}, async function (done, _converse) {
  274. await mock.waitForRoster(_converse, 'current');
  275. await mock.openControlBox(_converse);
  276. const muc_jid = 'xsf@muc.xmpp.org';
  277. const sender_jid = `${muc_jid}/romeo`;
  278. const impersonated_jid = `${muc_jid}/i_am_groot`
  279. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  280. const stanza = $pres({
  281. to: 'romeo@montague.lit/_converse.js-29092160',
  282. from: sender_jid
  283. })
  284. .c('x', {xmlns: Strophe.NS.MUC_USER})
  285. .c('item', {
  286. 'affiliation': 'owner',
  287. 'jid': 'newguy@montague.lit/_converse.js-290929789',
  288. 'role': 'participant'
  289. }).tree();
  290. _converse.connection._dataRecv(mock.createRequest(stanza));
  291. /*
  292. * <message to="romeo@montague.im/poezio" id="718d40df-3948-4798-a99b-35cc9f03cc4f-641" type="groupchat" from="xsf@muc.xmpp.org/romeo">
  293. * <received xmlns="urn:xmpp:carbons:2">
  294. * <forwarded xmlns="urn:xmpp:forward:0">
  295. * <message xmlns="jabber:client" to="xsf@muc.xmpp.org" type="groupchat" from="xsf@muc.xmpp.org/i_am_groot">
  296. * <body>I am groot.</body>
  297. * </message>
  298. * </forwarded>
  299. * </received>
  300. * </message>
  301. */
  302. const msg = $msg({
  303. 'from': sender_jid,
  304. 'id': _converse.connection.getUniqueId(),
  305. 'to': _converse.connection.jid,
  306. 'type': 'groupchat',
  307. 'xmlns': 'jabber:client'
  308. }).c('received', {'xmlns': 'urn:xmpp:carbons:2'})
  309. .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
  310. .c('message', {
  311. 'xmlns': 'jabber:client',
  312. 'from': impersonated_jid,
  313. 'to': muc_jid,
  314. 'type': 'groupchat'
  315. }).c('body').t('I am groot').tree();
  316. const view = _converse.api.chatviews.get(muc_jid);
  317. spyOn(converse.env.log, 'error');
  318. await _converse.handleMAMResult(view.model, { 'messages': [msg] });
  319. await u.waitUntil(() => converse.env.log.error.calls.count());
  320. expect(converse.env.log.error).toHaveBeenCalledWith(
  321. 'Invalid Stanza: MUC messages SHOULD NOT be XEP-0280 carbon copied'
  322. );
  323. expect(view.querySelectorAll('.chat-msg').length).toBe(0);
  324. expect(view.model.messages.length).toBe(0);
  325. done();
  326. }));
  327. it("keeps track of the sender's role and affiliation",
  328. mock.initConverse([], {}, async function (done, _converse) {
  329. const muc_jid = 'lounge@montague.lit';
  330. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  331. const view = _converse.api.chatviews.get(muc_jid);
  332. let msg = $msg({
  333. from: 'lounge@montague.lit/romeo',
  334. id: u.getUniqueId(),
  335. to: 'romeo@montague.lit',
  336. type: 'groupchat'
  337. }).c('body').t('I wrote this message!').tree();
  338. await view.model.handleMessageStanza(msg);
  339. await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
  340. expect(view.model.messages.last().occupant.get('affiliation')).toBe('owner');
  341. expect(view.model.messages.last().occupant.get('role')).toBe('moderator');
  342. expect(view.querySelectorAll('.chat-msg').length).toBe(1);
  343. expect(sizzle('.chat-msg', view.el).pop().classList.value.trim()).toBe('message chat-msg groupchat chat-msg--with-avatar moderator owner');
  344. let presence = $pres({
  345. to:'romeo@montague.lit/orchard',
  346. from:'lounge@montague.lit/romeo',
  347. id: u.getUniqueId()
  348. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  349. .c('item').attrs({
  350. affiliation: 'member',
  351. jid: 'romeo@montague.lit/orchard',
  352. role: 'participant'
  353. }).up()
  354. .c('status').attrs({code:'110'}).up()
  355. .c('status').attrs({code:'210'}).nodeTree;
  356. _converse.connection._dataRecv(mock.createRequest(presence));
  357. await u.waitUntil(() => view.model.messages.length === 4);
  358. msg = $msg({
  359. from: 'lounge@montague.lit/romeo',
  360. id: u.getUniqueId(),
  361. to: 'romeo@montague.lit',
  362. type: 'groupchat'
  363. }).c('body').t('Another message!').tree();
  364. await view.model.handleMessageStanza(msg);
  365. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  366. expect(view.model.messages.last().occupant.get('affiliation')).toBe('member');
  367. expect(view.model.messages.last().occupant.get('role')).toBe('participant');
  368. expect(view.querySelectorAll('.chat-msg').length).toBe(2);
  369. expect(sizzle('.chat-msg', view.el).pop().classList.value.trim()).toBe('message chat-msg groupchat chat-msg--with-avatar participant member');
  370. presence = $pres({
  371. to:'romeo@montague.lit/orchard',
  372. from:'lounge@montague.lit/romeo',
  373. id: u.getUniqueId()
  374. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  375. .c('item').attrs({
  376. affiliation: 'owner',
  377. jid: 'romeo@montague.lit/orchard',
  378. role: 'moderator'
  379. }).up()
  380. .c('status').attrs({code:'110'}).up()
  381. .c('status').attrs({code:'210'}).nodeTree;
  382. _converse.connection._dataRecv(mock.createRequest(presence));
  383. view.model.sendMessage('hello world');
  384. await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 3);
  385. const occupant = await u.waitUntil(() => view.model.messages.filter(m => m.get('type') === 'groupchat')[2].occupant);
  386. expect(occupant.get('affiliation')).toBe('owner');
  387. expect(occupant.get('role')).toBe('moderator');
  388. expect(view.querySelectorAll('.chat-msg').length).toBe(3);
  389. await u.waitUntil(() => sizzle('.chat-msg', view.el).pop().classList.value.trim() === 'message chat-msg groupchat chat-msg--with-avatar moderator owner');
  390. const add_events = view.model.occupants._events.add.length;
  391. msg = $msg({
  392. from: 'lounge@montague.lit/some1',
  393. id: u.getUniqueId(),
  394. to: 'romeo@montague.lit',
  395. type: 'groupchat'
  396. }).c('body').t('Message from someone not in the MUC right now').tree();
  397. await view.model.handleMessageStanza(msg);
  398. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  399. expect(view.model.messages.last().occupant).toBeUndefined();
  400. // Check that there's a new "add" event handler, for when the occupant appears.
  401. expect(view.model.occupants._events.add.length).toBe(add_events+1);
  402. // Check that the occupant gets added/removed to the message as it
  403. // gets removed or added.
  404. presence = $pres({
  405. to:'romeo@montague.lit/orchard',
  406. from:'lounge@montague.lit/some1',
  407. id: u.getUniqueId()
  408. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  409. .c('item').attrs({jid: 'some1@montague.lit/orchard'});
  410. _converse.connection._dataRecv(mock.createRequest(presence));
  411. await u.waitUntil(() => view.model.messages.last().occupant);
  412. expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now');
  413. expect(view.model.messages.last().occupant.get('nick')).toBe('some1');
  414. // Check that the "add" event handler was removed.
  415. expect(view.model.occupants._events.add.length).toBe(add_events);
  416. presence = $pres({
  417. to:'romeo@montague.lit/orchard',
  418. type: 'unavailable',
  419. from:'lounge@montague.lit/some1',
  420. id: u.getUniqueId()
  421. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  422. .c('item').attrs({jid: 'some1@montague.lit/orchard'});
  423. _converse.connection._dataRecv(mock.createRequest(presence));
  424. await u.waitUntil(() => !view.model.messages.last().occupant);
  425. expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now');
  426. expect(view.model.messages.last().occupant).toBeUndefined();
  427. // Check that there's a new "add" event handler, for when the occupant appears.
  428. expect(view.model.occupants._events.add.length).toBe(add_events+1);
  429. presence = $pres({
  430. to:'romeo@montague.lit/orchard',
  431. from:'lounge@montague.lit/some1',
  432. id: u.getUniqueId()
  433. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  434. .c('item').attrs({jid: 'some1@montague.lit/orchard'});
  435. _converse.connection._dataRecv(mock.createRequest(presence));
  436. await u.waitUntil(() => view.model.messages.last().occupant);
  437. expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now');
  438. expect(view.model.messages.last().occupant.get('nick')).toBe('some1');
  439. // Check that the "add" event handler was removed.
  440. expect(view.model.occupants._events.add.length).toBe(add_events);
  441. done();
  442. }));
  443. it("keeps track whether you are the sender or not",
  444. mock.initConverse([], {}, async function (done, _converse) {
  445. const muc_jid = 'lounge@montague.lit';
  446. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  447. const view = _converse.api.chatviews.get(muc_jid);
  448. const msg = $msg({
  449. from: 'lounge@montague.lit/romeo',
  450. id: u.getUniqueId(),
  451. to: 'romeo@montague.lit',
  452. type: 'groupchat'
  453. }).c('body').t('I wrote this message!').tree();
  454. await view.model.handleMessageStanza(msg);
  455. await u.waitUntil(() => view.model.messages.last()?.get('received'));
  456. expect(view.model.messages.last().get('sender')).toBe('me');
  457. done();
  458. }));
  459. it("will be shown as received upon MUC reflection",
  460. mock.initConverse([], {}, async function (done, _converse) {
  461. await mock.waitForRoster(_converse, 'current');
  462. const muc_jid = 'lounge@montague.lit';
  463. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  464. const view = _converse.api.chatviews.get(muc_jid);
  465. const textarea = view.querySelector('textarea.chat-textarea');
  466. textarea.value = 'But soft, what light through yonder airlock breaks?';
  467. view.onKeyDown({
  468. target: textarea,
  469. preventDefault: function preventDefault () {},
  470. keyCode: 13 // Enter
  471. });
  472. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  473. expect(view.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(0);
  474. const msg_obj = view.model.messages.at(0);
  475. const stanza = u.toStanza(`
  476. <message xmlns="jabber:client"
  477. from="${msg_obj.get('from')}"
  478. to="${_converse.connection.jid}"
  479. type="groupchat">
  480. <body>${msg_obj.get('message')}</body>
  481. <stanza-id xmlns="urn:xmpp:sid:0"
  482. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  483. by="lounge@montague.lit"/>
  484. <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
  485. </message>`);
  486. await view.model.handleMessageStanza(stanza);
  487. await u.waitUntil(() => view.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
  488. expect(view.querySelectorAll('.chat-msg__receipt').length).toBe(0);
  489. expect(view.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(1);
  490. expect(view.model.messages.length).toBe(1);
  491. const message = view.model.messages.at(0);
  492. expect(message.get('stanza_id lounge@montague.lit')).toBe('5f3dbc5e-e1d3-4077-a492-693f3769c7ad');
  493. expect(message.get('origin_id')).toBe(msg_obj.get('origin_id'));
  494. done();
  495. }));
  496. it("gets updated with its stanza-id upon MUC reflection",
  497. mock.initConverse([], {}, async function (done, _converse) {
  498. const muc_jid = 'room@muc.example.com';
  499. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  500. const view = _converse.api.chatviews.get(muc_jid);
  501. view.model.sendMessage('hello world');
  502. await u.waitUntil(() => view.model.messages.length === 1);
  503. const msg = view.model.messages.at(0);
  504. expect(msg.get('stanza_id')).toBeUndefined();
  505. expect(msg.get('origin_id')).toBe(msg.get('origin_id'));
  506. const stanza = u.toStanza(`
  507. <message xmlns="jabber:client"
  508. from="room@muc.example.com/romeo"
  509. to="${_converse.connection.jid}"
  510. type="groupchat">
  511. <body>Hello world</body>
  512. <stanza-id xmlns="urn:xmpp:sid:0"
  513. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  514. by="room@muc.example.com"/>
  515. <origin-id xmlns="urn:xmpp:sid:0" id="${msg.get('origin_id')}"/>
  516. </message>`);
  517. spyOn(view.model, 'updateMessage').and.callThrough();
  518. _converse.connection._dataRecv(mock.createRequest(stanza));
  519. await u.waitUntil(() => view.model.updateMessage.calls.count() === 1);
  520. expect(view.model.messages.length).toBe(1);
  521. expect(view.model.messages.at(0).get('stanza_id room@muc.example.com')).toBe("5f3dbc5e-e1d3-4077-a492-693f3769c7ad");
  522. expect(view.model.messages.at(0).get('origin_id')).toBe(msg.get('origin_id'));
  523. done();
  524. }));
  525. it("can cause a delivery receipt to be returned",
  526. mock.initConverse([], {}, async function (done, _converse) {
  527. await mock.waitForRoster(_converse, 'current');
  528. const muc_jid = 'lounge@montague.lit';
  529. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  530. const view = _converse.api.chatviews.get(muc_jid);
  531. const textarea = view.querySelector('textarea.chat-textarea');
  532. textarea.value = 'But soft, what light through yonder airlock breaks?';
  533. view.onKeyDown({
  534. target: textarea,
  535. preventDefault: function preventDefault () {},
  536. keyCode: 13 // Enter
  537. });
  538. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  539. expect(view.querySelectorAll('.chat-msg').length).toBe(1);
  540. const msg_obj = view.model.messages.at(0);
  541. let stanza = u.toStanza(`
  542. <message xmlns="jabber:client"
  543. from="${msg_obj.get('from')}"
  544. to="${_converse.connection.jid}"
  545. type="groupchat">
  546. <body>${msg_obj.get('message')}</body>
  547. <stanza-id xmlns="urn:xmpp:sid:0"
  548. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  549. by="lounge@montague.lit"/>
  550. <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
  551. </message>`);
  552. await view.model.handleMessageStanza(stanza);
  553. await u.waitUntil(() => view.model.messages.last().get('received'));
  554. stanza = u.toStanza(`
  555. <message xml:lang="en" to="romeo@montague.lit/orchard"
  556. from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
  557. <received xmlns="urn:xmpp:receipts" id="${msg_obj.get('msgid')}"/>
  558. <origin-id xmlns="urn:xmpp:sid:0" id="CE08D448-5ED8-4B6A-BB5B-07ED9DFE4FF0"/>
  559. </message>`);
  560. _converse.connection._dataRecv(mock.createRequest(stanza));
  561. expect(view.querySelectorAll('.chat-msg').length).toBe(1);
  562. done();
  563. }));
  564. });