/*global mock */ const { Promise, Strophe, $msg, $pres, sizzle, stanza_utils } = converse.env; const u = converse.env.utils; describe("A Groupchat Message", function () { describe("which is succeeded by an error message", function () { it("will have the error displayed below it", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const view = _converse.api.chatviews.get(muc_jid); const textarea = view.el.querySelector('textarea.chat-textarea'); textarea.value = 'hello world' const enter_event = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'stopPropagation': function stopPropagation () {}, 'keyCode': 13 // Enter } view.onKeyDown(enter_event); await new Promise(resolve => view.once('messageInserted', resolve)); const msg = view.model.messages.at(0); const err_msg_text = "Message rejected because you're sending messages too quickly"; const error = u.toStanza(` ${err_msg_text} hello world `); _converse.connection._dataRecv(mock.createRequest(error)); expect(await u.waitUntil(() => view.el.querySelector('.chat-msg__error')?.textContent?.trim())).toBe(err_msg_text); expect(view.model.messages.length).toBe(1); const message = view.model.messages.at(0); expect(message.get('received')).toBeUndefined(); expect(message.get('body')).toBe('hello world'); expect(message.get('error')).toBe(err_msg_text); done(); })); }); describe("an info message", function () { it("is not rendered as a followup message", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const view = _converse.api.chatviews.get(muc_jid); let presence = u.toStanza(` `); _converse.connection._dataRecv(mock.createRequest(presence)); await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 1); presence = u.toStanza(` `); _converse.connection._dataRecv(mock.createRequest(presence)); await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 2); const messages = view.el.querySelectorAll('.chat-info'); expect(u.hasClass('chat-msg--followup', messages[0])).toBe(false); expect(u.hasClass('chat-msg--followup', messages[1])).toBe(false); done(); })); it("is not shown if its a duplicate", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const view = _converse.api.chatviews.get(muc_jid); const presence = u.toStanza(` `); // XXX: We wait for createInfoMessages to complete, if we don't // we still get two info messages due to messages // created from presences not being queued and run // sequentially (i.e. by waiting for promises to resolve) // like we do with message stanzas. spyOn(view.model, 'createInfoMessages').and.callThrough(); _converse.connection._dataRecv(mock.createRequest(presence)); await u.waitUntil(() => view.model.createInfoMessages.calls.count()); await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 1); _converse.connection._dataRecv(mock.createRequest(presence)); await u.waitUntil(() => view.model.createInfoMessages.calls.count() === 2); expect(view.el.querySelectorAll('.chat-info').length).toBe(1); done(); })); }); it("is rejected if it's an unencapsulated forwarded message", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const impersonated_jid = `${muc_jid}/alice`; const received_stanza = u.toStanza(` Yet I should kill thee with much cherishing. `); const view = _converse.api.chatviews.get(muc_jid); spyOn(view.model, 'onMessage').and.callThrough(); spyOn(converse.env.log, 'error'); _converse.connection._dataRecv(mock.createRequest(received_stanza)); await u.waitUntil(() => view.model.onMessage.calls.count() === 1); expect(converse.env.log.error).toHaveBeenCalledWith( `Ignoring unencapsulated forwarded message from ${muc_jid}/mallory` ); expect(view.el.querySelectorAll('.chat-msg').length).toBe(0); expect(view.model.messages.length).toBe(0); done(); })); it("can contain a chat state notification and will still be shown", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const view = _converse.api.chatviews.get(muc_jid); if (!view.el.querySelectorAll('.chat-area').length) { view.renderChatArea(); } const message = 'romeo: Your attention is required'; const nick = mock.chatroom_names[0], msg = $msg({ from: 'lounge@montague.lit/'+nick, id: u.getUniqueId(), to: 'romeo@montague.lit', type: 'groupchat' }).c('body').t(message) .c('active', {'xmlns': "http://jabber.org/protocol/chatstates"}) .tree(); await view.model.handleMessageStanza(msg); await new Promise(resolve => view.once('messageInserted', resolve)); expect(view.el.querySelector('.chat-msg')).not.toBe(null); done(); })); it("is specially marked when you are mentioned in it", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const view = _converse.api.chatviews.get(muc_jid); if (!view.el.querySelectorAll('.chat-area').length) { view.renderChatArea(); } const message = 'romeo: Your attention is required'; const nick = mock.chatroom_names[0], msg = $msg({ from: 'lounge@montague.lit/'+nick, id: u.getUniqueId(), to: 'romeo@montague.lit', type: 'groupchat' }).c('body').t(message).tree(); await view.model.handleMessageStanza(msg); await new Promise(resolve => view.once('messageInserted', resolve)); expect(u.hasClass('mentioned', view.el.querySelector('.chat-msg'))).toBeTruthy(); done(); })); it("can not be expected to have a unique id attribute", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const view = _converse.api.chatviews.get(muc_jid); if (!view.el.querySelectorAll('.chat-area').length) { view.renderChatArea(); } const id = u.getUniqueId(); let msg = $msg({ from: 'lounge@montague.lit/some1', id: id, to: 'romeo@montague.lit', type: 'groupchat' }).c('body').t('First message').tree(); await view.model.handleMessageStanza(msg); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); msg = $msg({ from: 'lounge@montague.lit/some2', id: id, to: 'romeo@montague.lit', type: 'groupchat' }).c('body').t('Another message').tree(); await view.model.handleMessageStanza(msg); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2); expect(view.model.messages.length).toBe(2); done(); })); it("is ignored if it has the same archive-id of an already received one", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { const muc_jid = 'room@muc.example.com'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const view = _converse.api.chatviews.get(muc_jid); spyOn(view.model, 'getDuplicateMessage').and.callThrough(); let stanza = u.toStanza(` Typical body text `); _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => view.model.messages.length === 1); await u.waitUntil(() => view.model.getDuplicateMessage.calls.count() === 1); let result = await view.model.getDuplicateMessage.calls.all()[0].returnValue; expect(result).toBe(undefined); stanza = u.toStanza(` Typical body text `); spyOn(view.model, 'updateMessage'); view.model.handleMAMResult({ 'messages': [stanza] }); await u.waitUntil(() => view.model.getDuplicateMessage.calls.count() === 2); result = await view.model.getDuplicateMessage.calls.all()[1].returnValue; expect(result instanceof _converse.Message).toBe(true); expect(view.model.messages.length).toBe(1); await u.waitUntil(() => view.model.updateMessage.calls.count()); done(); })); it("is ignored if it has the same stanza-id of an already received one", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { const muc_jid = 'room@muc.example.com'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const view = _converse.api.chatviews.get(muc_jid); spyOn(view.model, 'getStanzaIdQueryAttrs').and.callThrough(); let stanza = u.toStanza(` Typical body text `); _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => view.model.messages.length === 1); await u.waitUntil(() => view.model.getStanzaIdQueryAttrs.calls.count() === 1); let result = await view.model.getStanzaIdQueryAttrs.calls.all()[0].returnValue; expect(result instanceof Array).toBe(true); expect(result[0] instanceof Object).toBe(true); expect(result[0]['stanza_id room@muc.example.com']).toBe("5f3dbc5e-e1d3-4077-a492-693f3769c7ad"); stanza = u.toStanza(` Typical body text `); spyOn(view.model, 'updateMessage'); spyOn(view.model, 'getDuplicateMessage').and.callThrough(); _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => view.model.getDuplicateMessage.calls.count()); result = await view.model.getDuplicateMessage.calls.all()[0].returnValue; expect(result instanceof _converse.Message).toBe(true); expect(view.model.messages.length).toBe(1); await u.waitUntil(() => view.model.updateMessage.calls.count()); done(); })); it("will be discarded if it's a malicious message meant to look like a carbon copy", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { await mock.waitForRoster(_converse, 'current'); await mock.openControlBox(_converse); const muc_jid = 'xsf@muc.xmpp.org'; const sender_jid = `${muc_jid}/romeo`; const impersonated_jid = `${muc_jid}/i_am_groot` await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const stanza = $pres({ to: 'romeo@montague.lit/_converse.js-29092160', from: sender_jid }) .c('x', {xmlns: Strophe.NS.MUC_USER}) .c('item', { 'affiliation': 'none', 'jid': 'newguy@montague.lit/_converse.js-290929789', 'role': 'participant' }).tree(); _converse.connection._dataRecv(mock.createRequest(stanza)); /* * * * * * I am groot. * * * * */ const msg = $msg({ 'from': sender_jid, 'id': _converse.connection.getUniqueId(), 'to': _converse.connection.jid, 'type': 'groupchat', 'xmlns': 'jabber:client' }).c('received', {'xmlns': 'urn:xmpp:carbons:2'}) .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) .c('message', { 'xmlns': 'jabber:client', 'from': impersonated_jid, 'to': muc_jid, 'type': 'groupchat' }).c('body').t('I am groot').tree(); const view = _converse.api.chatviews.get(muc_jid); spyOn(converse.env.log, 'error'); await view.model.handleMAMResult({ 'messages': [msg] }); expect(converse.env.log.error).toHaveBeenCalledWith( 'Invalid Stanza: MUC messages SHOULD NOT be XEP-0280 carbon copied' ); expect(view.el.querySelectorAll('.chat-msg').length).toBe(0); expect(view.model.messages.length).toBe(0); done(); })); it("keeps track of the sender's role and affiliation", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const view = _converse.api.chatviews.get(muc_jid); let msg = $msg({ from: 'lounge@montague.lit/romeo', id: u.getUniqueId(), to: 'romeo@montague.lit', type: 'groupchat' }).c('body').t('I wrote this message!').tree(); await view.model.handleMessageStanza(msg); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length); expect(view.model.messages.last().occupant.get('affiliation')).toBe('owner'); expect(view.model.messages.last().occupant.get('role')).toBe('moderator'); expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); expect(sizzle('.chat-msg', view.el).pop().classList.value.trim()).toBe('message chat-msg groupchat moderator owner'); let presence = $pres({ to:'romeo@montague.lit/orchard', from:'lounge@montague.lit/romeo', id: u.getUniqueId() }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) .c('item').attrs({ affiliation: 'member', jid: 'romeo@montague.lit/orchard', role: 'participant' }).up() .c('status').attrs({code:'110'}).up() .c('status').attrs({code:'210'}).nodeTree; _converse.connection._dataRecv(mock.createRequest(presence)); msg = $msg({ from: 'lounge@montague.lit/romeo', id: u.getUniqueId(), to: 'romeo@montague.lit', type: 'groupchat' }).c('body').t('Another message!').tree(); await view.model.handleMessageStanza(msg); await new Promise(resolve => view.once('messageInserted', resolve)); expect(view.model.messages.last().occupant.get('affiliation')).toBe('member'); expect(view.model.messages.last().occupant.get('role')).toBe('participant'); expect(view.el.querySelectorAll('.chat-msg').length).toBe(2); expect(sizzle('.chat-msg', view.el).pop().classList.value.trim()).toBe('message chat-msg groupchat participant member'); presence = $pres({ to:'romeo@montague.lit/orchard', from:'lounge@montague.lit/romeo', id: u.getUniqueId() }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) .c('item').attrs({ affiliation: 'owner', jid: 'romeo@montague.lit/orchard', role: 'moderator' }).up() .c('status').attrs({code:'110'}).up() .c('status').attrs({code:'210'}).nodeTree; _converse.connection._dataRecv(mock.createRequest(presence)); view.model.sendMessage('hello world'); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 3); const occupant = await u.waitUntil(() => view.model.messages.filter(m => m.get('type') === 'groupchat')[2].occupant); expect(occupant.get('affiliation')).toBe('owner'); expect(occupant.get('role')).toBe('moderator'); expect(view.el.querySelectorAll('.chat-msg').length).toBe(3); await u.waitUntil(() => sizzle('.chat-msg', view.el).pop().classList.value.trim() === 'message chat-msg groupchat moderator owner'); const add_events = view.model.occupants._events.add.length; msg = $msg({ from: 'lounge@montague.lit/some1', id: u.getUniqueId(), to: 'romeo@montague.lit', type: 'groupchat' }).c('body').t('Message from someone not in the MUC right now').tree(); await view.model.handleMessageStanza(msg); await new Promise(resolve => view.once('messageInserted', resolve)); expect(view.model.messages.last().occupant).toBeUndefined(); // Check that there's a new "add" event handler, for when the occupant appears. expect(view.model.occupants._events.add.length).toBe(add_events+1); // Check that the occupant gets added/removed to the message as it // gets removed or added. presence = $pres({ to:'romeo@montague.lit/orchard', from:'lounge@montague.lit/some1', id: u.getUniqueId() }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) .c('item').attrs({jid: 'some1@montague.lit/orchard'}); _converse.connection._dataRecv(mock.createRequest(presence)); await u.waitUntil(() => view.model.messages.last().occupant); expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now'); expect(view.model.messages.last().occupant.get('nick')).toBe('some1'); // Check that the "add" event handler was removed. expect(view.model.occupants._events.add.length).toBe(add_events); presence = $pres({ to:'romeo@montague.lit/orchard', type: 'unavailable', from:'lounge@montague.lit/some1', id: u.getUniqueId() }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) .c('item').attrs({jid: 'some1@montague.lit/orchard'}); _converse.connection._dataRecv(mock.createRequest(presence)); await u.waitUntil(() => !view.model.messages.last().occupant); expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now'); expect(view.model.messages.last().occupant).toBeUndefined(); // Check that there's a new "add" event handler, for when the occupant appears. expect(view.model.occupants._events.add.length).toBe(add_events+1); presence = $pres({ to:'romeo@montague.lit/orchard', from:'lounge@montague.lit/some1', id: u.getUniqueId() }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) .c('item').attrs({jid: 'some1@montague.lit/orchard'}); _converse.connection._dataRecv(mock.createRequest(presence)); await u.waitUntil(() => view.model.messages.last().occupant); expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now'); expect(view.model.messages.last().occupant.get('nick')).toBe('some1'); // Check that the "add" event handler was removed. expect(view.model.occupants._events.add.length).toBe(add_events); done(); })); it("keeps track whether you are the sender or not", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const view = _converse.api.chatviews.get(muc_jid); const msg = $msg({ from: 'lounge@montague.lit/romeo', id: u.getUniqueId(), to: 'romeo@montague.lit', type: 'groupchat' }).c('body').t('I wrote this message!').tree(); await view.model.handleMessageStanza(msg); expect(view.model.messages.last().get('sender')).toBe('me'); done(); })); it("can be replaced with a correction", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const view = _converse.api.chatviews.get(muc_jid); const stanza = $pres({ to: 'romeo@montague.lit/_converse.js-29092160', from: 'coven@chat.shakespeare.lit/newguy' }) .c('x', {xmlns: Strophe.NS.MUC_USER}) .c('item', { 'affiliation': 'none', 'jid': 'newguy@montague.lit/_converse.js-290929789', 'role': 'participant' }).tree(); _converse.connection._dataRecv(mock.createRequest(stanza)); const msg_id = u.getUniqueId(); await view.model.handleMessageStanza($msg({ 'from': 'lounge@montague.lit/newguy', 'to': _converse.connection.jid, 'type': 'groupchat', 'id': msg_id, }).c('body').t('But soft, what light through yonder airlock breaks?').tree()); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length); expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); expect(view.el.querySelector('.chat-msg__text').textContent) .toBe('But soft, what light through yonder airlock breaks?'); await view.model.handleMessageStanza($msg({ 'from': 'lounge@montague.lit/newguy', 'to': _converse.connection.jid, 'type': 'groupchat', 'id': u.getUniqueId(), }).c('body').t('But soft, what light through yonder chimney breaks?').up() .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree()); await u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent === 'But soft, what light through yonder chimney breaks?', 500); expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1); await view.model.handleMessageStanza($msg({ 'from': 'lounge@montague.lit/newguy', 'to': _converse.connection.jid, 'type': 'groupchat', 'id': u.getUniqueId(), }).c('body').t('But soft, what light through yonder window breaks?').up() .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree()); await u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent === 'But soft, what light through yonder window breaks?', 500); expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1); view.el.querySelector('.chat-msg__content .fa-edit').click(); const modal = view.model.messages.at(0).message_versions_modal; await u.waitUntil(() => u.isVisible(modal.el), 1000); const older_msgs = modal.el.querySelectorAll('.older-msg'); expect(older_msgs.length).toBe(2); expect(older_msgs[0].childNodes[2].textContent).toBe('But soft, what light through yonder airlock breaks?'); expect(older_msgs[0].childNodes[0].nodeName).toBe('TIME'); expect(older_msgs[1].childNodes[0].nodeName).toBe('TIME'); expect(older_msgs[1].childNodes[2].textContent).toBe('But soft, what light through yonder chimney breaks?'); done(); })); it("can be sent as a correction by using the up arrow", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const view = _converse.api.chatviews.get(muc_jid); const textarea = view.el.querySelector('textarea.chat-textarea'); expect(textarea.value).toBe(''); view.onKeyDown({ target: textarea, keyCode: 38 // Up arrow }); expect(textarea.value).toBe(''); textarea.value = 'But soft, what light through yonder airlock breaks?'; view.onKeyDown({ target: textarea, preventDefault: function preventDefault () {}, keyCode: 13 // Enter }); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); expect(view.el.querySelector('.chat-msg__text').textContent) .toBe('But soft, what light through yonder airlock breaks?'); const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'}); expect(textarea.value).toBe(''); view.onKeyDown({ target: textarea, keyCode: 38 // Up arrow }); await new Promise(resolve => view.model.messages.once('rendered', resolve)); expect(textarea.value).toBe('But soft, what light through yonder airlock breaks?'); expect(view.model.messages.at(0).get('correcting')).toBe(true); expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(true); spyOn(_converse.connection, 'send'); textarea.value = 'But soft, what light through yonder window breaks?'; view.onKeyDown({ target: textarea, preventDefault: function preventDefault () {}, keyCode: 13 // Enter }); expect(_converse.connection.send).toHaveBeenCalled(); await new Promise(resolve => view.model.messages.once('rendered', resolve)); const msg = _converse.connection.send.calls.all()[0].args[0]; expect(msg.toLocaleString()) .toBe(``+ `But soft, what light through yonder window breaks?`+ ``+ ``+ ``+ ``); expect(view.model.messages.models.length).toBe(1); const corrected_message = view.model.messages.at(0); expect(corrected_message.get('msgid')).toBe(first_msg.get('msgid')); expect(corrected_message.get('correcting')).toBe(false); const older_versions = corrected_message.get('older_versions'); const keys = Object.keys(older_versions); expect(keys.length).toBe(1); expect(older_versions[keys[0]]).toBe('But soft, what light through yonder airlock breaks?'); expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(false); // Check that messages from other users are skipped await view.model.handleMessageStanza($msg({ 'from': muc_jid+'/someone-else', 'id': u.getUniqueId(), 'to': 'romeo@montague.lit', 'type': 'groupchat' }).c('body').t('Hello world').tree()); await new Promise(resolve => view.once('messageInserted', resolve)); expect(view.el.querySelectorAll('.chat-msg').length).toBe(2); // Test that pressing the down arrow cancels message correction expect(textarea.value).toBe(''); view.onKeyDown({ target: textarea, keyCode: 38 // Up arrow }); expect(textarea.value).toBe('But soft, what light through yonder window breaks?'); expect(view.model.messages.at(0).get('correcting')).toBe(true); expect(view.el.querySelectorAll('.chat-msg').length).toBe(2); await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500); expect(textarea.value).toBe('But soft, what light through yonder window breaks?'); view.onKeyDown({ target: textarea, keyCode: 40 // Down arrow }); expect(textarea.value).toBe(''); expect(view.model.messages.at(0).get('correcting')).toBe(false); expect(view.el.querySelectorAll('.chat-msg').length).toBe(2); await u.waitUntil(() => !u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500); done(); })); it("will be shown as received upon MUC reflection", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { await mock.waitForRoster(_converse, 'current'); const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const view = _converse.api.chatviews.get(muc_jid); const textarea = view.el.querySelector('textarea.chat-textarea'); textarea.value = 'But soft, what light through yonder airlock breaks?'; view.onKeyDown({ target: textarea, preventDefault: function preventDefault () {}, keyCode: 13 // Enter }); await new Promise(resolve => view.once('messageInserted', resolve)); expect(view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(0); const msg_obj = view.model.messages.at(0); const stanza = u.toStanza(` ${msg_obj.get('message')} `); await view.model.handleMessageStanza(stanza); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500); expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); expect(view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(1); expect(view.model.messages.length).toBe(1); const message = view.model.messages.at(0); expect(message.get('stanza_id lounge@montague.lit')).toBe('5f3dbc5e-e1d3-4077-a492-693f3769c7ad'); expect(message.get('origin_id')).toBe(msg_obj.get('origin_id')); done(); })); it("gets updated with its stanza-id upon MUC reflection", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { const muc_jid = 'room@muc.example.com'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const view = _converse.api.chatviews.get(muc_jid); view.model.sendMessage('hello world'); await u.waitUntil(() => view.model.messages.length === 1); const msg = view.model.messages.at(0); expect(msg.get('stanza_id')).toBeUndefined(); expect(msg.get('origin_id')).toBe(msg.get('origin_id')); const stanza = u.toStanza(` Hello world `); spyOn(view.model, 'updateMessage').and.callThrough(); _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => view.model.updateMessage.calls.count() === 1); expect(view.model.messages.length).toBe(1); expect(view.model.messages.at(0).get('stanza_id room@muc.example.com')).toBe("5f3dbc5e-e1d3-4077-a492-693f3769c7ad"); expect(view.model.messages.at(0).get('origin_id')).toBe(msg.get('origin_id')); done(); })); it("can cause a delivery receipt to be returned", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { await mock.waitForRoster(_converse, 'current'); const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const view = _converse.api.chatviews.get(muc_jid); const textarea = view.el.querySelector('textarea.chat-textarea'); textarea.value = 'But soft, what light through yonder airlock breaks?'; view.onKeyDown({ target: textarea, preventDefault: function preventDefault () {}, keyCode: 13 // Enter }); await new Promise(resolve => view.once('messageInserted', resolve)); expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); const msg_obj = view.model.messages.at(0); const stanza = u.toStanza(` `); spyOn(stanza_utils, "parseMUCMessage").and.callThrough(); _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => stanza_utils.parseMUCMessage.calls.count() === 1); expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); done(); })); it("can cause a chat marker to be returned", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { await mock.waitForRoster(_converse, 'current'); const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const view = _converse.api.chatviews.get(muc_jid); const textarea = view.el.querySelector('textarea.chat-textarea'); textarea.value = 'But soft, what light through yonder airlock breaks?'; view.onKeyDown({ target: textarea, preventDefault: function preventDefault () {}, keyCode: 13 // Enter }); await new Promise(resolve => view.once('messageInserted', resolve)); expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); expect(view.el.querySelector('.chat-msg .chat-msg__body').textContent.trim()) .toBe("But soft, what light through yonder airlock breaks?"); const msg_obj = view.model.messages.at(0); let stanza = u.toStanza(` `); const stanza_utils = converse.env.stanza_utils; spyOn(stanza_utils, "getChatMarker").and.callThrough(); _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 1); expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); stanza = u.toStanza(` `); _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 2); expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); stanza = u.toStanza(` `); _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 3); expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); stanza = u.toStanza(` 'tis I! `); _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 4); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2); expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); done(); })); describe("when received", function () { it("highlights all users mentioned via XEP-0372 references", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'tom'); const view = _converse.api.chatviews.get(muc_jid); ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].forEach((nick) => { _converse.connection._dataRecv(mock.createRequest( $pres({ 'to': 'tom@montague.lit/resource', 'from': `lounge@montague.lit/${nick}` }) .c('x', {xmlns: Strophe.NS.MUC_USER}) .c('item', { 'affiliation': 'none', 'jid': `${nick}@montague.lit/resource`, 'role': 'participant' })) ); }); const msg = $msg({ from: 'lounge@montague.lit/gibson', id: u.getUniqueId(), to: 'romeo@montague.lit', type: 'groupchat' }).c('body').t('hello z3r0 tom mr.robot, how are you?').up() .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'6', 'end':'10', 'type':'mention', 'uri':'xmpp:z3r0@montague.lit'}).up() .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'11', 'end':'14', 'type':'mention', 'uri':'xmpp:romeo@montague.lit'}).up() .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'15', 'end':'23', 'type':'mention', 'uri':'xmpp:mr.robot@montague.lit'}).nodeTree; await view.model.handleMessageStanza(msg); const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text')); expect(message.classList.length).toEqual(1); expect(message.innerHTML).toBe( 'hello z3r0 '+ 'tom '+ 'mr.robot, how are you?'); done(); })); it("highlights all users mentioned via XEP-0372 references in a quoted message", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'tom'); const view = _converse.api.chatviews.get(muc_jid); ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].forEach((nick) => { _converse.connection._dataRecv(mock.createRequest( $pres({ 'to': 'tom@montague.lit/resource', 'from': `lounge@montague.lit/${nick}` }) .c('x', {xmlns: Strophe.NS.MUC_USER}) .c('item', { 'affiliation': 'none', 'jid': `${nick}@montague.lit/resource`, 'role': 'participant' })) ); }); const msg = $msg({ from: 'lounge@montague.lit/gibson', id: u.getUniqueId(), to: 'romeo@montague.lit', type: 'groupchat' }).c('body').t('>hello z3r0 tom mr.robot, how are you?').up() .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'7', 'end':'11', 'type':'mention', 'uri':'xmpp:z3r0@montague.lit'}).up() .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'12', 'end':'15', 'type':'mention', 'uri':'xmpp:romeo@montague.lit'}).up() .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'16', 'end':'24', 'type':'mention', 'uri':'xmpp:mr.robot@montague.lit'}).nodeTree; await view.model.handleMessageStanza(msg); const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text')); expect(message.classList.length).toEqual(1); expect(message.innerHTML).toBe( '>hello z3r0 '+ 'tom '+ 'mr.robot, how are you?'); done(); })); }); describe("in which someone is mentioned", function () { it("gets parsed for mentions which get turned into references", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'tom'); const view = _converse.api.chatviews.get(muc_jid); ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh', 'Link Mauve'].forEach((nick) => { _converse.connection._dataRecv(mock.createRequest( $pres({ 'to': 'tom@montague.lit/resource', 'from': `lounge@montague.lit/${nick}` }) .c('x', {xmlns: Strophe.NS.MUC_USER}) .c('item', { 'affiliation': 'none', 'jid': `${nick.replace(/\s/g, '-')}@montague.lit/resource`, 'role': 'participant' }))); }); // Also check that nicks from received messages, (but for which // we don't have occupant objects) can be mentioned. const stanza = u.toStanza(` Boo! `); await view.model.handleMessageStanza(stanza); // Run a few unit tests for the parseTextForReferences method let [text, references] = view.model.parseTextForReferences('hello z3r0') expect(references.length).toBe(0); expect(text).toBe('hello z3r0'); [text, references] = view.model.parseTextForReferences('hello @z3r0') expect(references.length).toBe(1); expect(text).toBe('hello z3r0'); expect(JSON.stringify(references)) .toBe('[{"begin":6,"end":10,"value":"z3r0","type":"mention","uri":"xmpp:z3r0@montague.lit"}]'); [text, references] = view.model.parseTextForReferences('hello @some1 @z3r0 @gibson @mr.robot, how are you?') expect(text).toBe('hello @some1 z3r0 gibson mr.robot, how are you?'); expect(JSON.stringify(references)) .toBe('[{"begin":13,"end":17,"value":"z3r0","type":"mention","uri":"xmpp:z3r0@montague.lit"},'+ '{"begin":18,"end":24,"value":"gibson","type":"mention","uri":"xmpp:gibson@montague.lit"},'+ '{"begin":25,"end":33,"value":"mr.robot","type":"mention","uri":"xmpp:mr.robot@montague.lit"}]'); [text, references] = view.model.parseTextForReferences('yo @gib') expect(text).toBe('yo @gib'); expect(references.length).toBe(0); [text, references] = view.model.parseTextForReferences('yo @gibsonian') expect(text).toBe('yo @gibsonian'); expect(references.length).toBe(0); [text, references] = view.model.parseTextForReferences('@gibson') expect(text).toBe('gibson'); expect(references.length).toBe(1); expect(JSON.stringify(references)) .toBe('[{"begin":0,"end":6,"value":"gibson","type":"mention","uri":"xmpp:gibson@montague.lit"}]'); [text, references] = view.model.parseTextForReferences('hi @Link Mauve how are you?') expect(text).toBe('hi Link Mauve how are you?'); expect(references.length).toBe(1); expect(JSON.stringify(references)) .toBe('[{"begin":3,"end":13,"value":"Link Mauve","type":"mention","uri":"xmpp:Link-Mauve@montague.lit"}]'); [text, references] = view.model.parseTextForReferences('https://example.org/@gibson') expect(text).toBe('https://example.org/@gibson'); expect(references.length).toBe(0); expect(JSON.stringify(references)) .toBe('[]'); [text, references] = view.model.parseTextForReferences('mail@gibson.com') expect(text).toBe('mail@gibson.com'); expect(references.length).toBe(0); expect(JSON.stringify(references)) .toBe('[]'); [text, references] = view.model.parseTextForReferences( 'https://linkmauve.fr@Link Mauve/ https://linkmauve.fr/@github/is_back gibson@gibson.com gibson@Link Mauve.fr') expect(text).toBe( 'https://linkmauve.fr@Link Mauve/ https://linkmauve.fr/@github/is_back gibson@gibson.com gibson@Link Mauve.fr'); expect(references.length).toBe(0); expect(JSON.stringify(references)) .toBe('[]'); [text, references] = view.model.parseTextForReferences('@gh0st where are you?') expect(text).toBe('gh0st where are you?'); expect(references.length).toBe(1); expect(JSON.stringify(references)) .toBe('[{"begin":0,"end":5,"value":"gh0st","type":"mention","uri":"xmpp:lounge@montague.lit/gh0st"}]'); done(); })); it("parses for mentions as indicated with an @ preceded by a space or at the start of the text", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'tom'); const view = _converse.api.chatviews.get(muc_jid); ['NotAnAdress', 'darnuria'].forEach((nick) => { _converse.connection._dataRecv(mock.createRequest( $pres({ 'to': 'tom@montague.lit/resource', 'from': `lounge@montague.lit/${nick}` }) .c('x', {xmlns: Strophe.NS.MUC_USER}) .c('item', { 'affiliation': 'none', 'jid': `${nick.replace(/\s/g, '-')}@montague.lit/resource`, 'role': 'participant' }))); }); // Test that we don't match @nick in email adresses. let [text, references] = view.model.parseTextForReferences('contact contact@NotAnAdress.eu'); expect(references.length).toBe(0); expect(text).toBe('contact contact@NotAnAdress.eu'); // Test that we don't match @nick in url [text, references] = view.model.parseTextForReferences('nice website https://darnuria.eu/@darnuria'); expect(references.length).toBe(0); expect(text).toBe('nice website https://darnuria.eu/@darnuria'); done(); })); it("properly encodes the URIs in sent out references", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'tom'); const view = _converse.api.roomviews.get(muc_jid); _converse.connection._dataRecv(mock.createRequest( $pres({ 'to': 'tom@montague.lit/resource', 'from': `lounge@montague.lit/Link Mauve` }) .c('x', {xmlns: Strophe.NS.MUC_USER}) .c('item', { 'affiliation': 'none', 'role': 'participant' }))); await u.waitUntil(() => view.model.occupants.length === 2); const textarea = view.el.querySelector('textarea.chat-textarea'); textarea.value = 'hello @Link Mauve' const enter_event = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'stopPropagation': function stopPropagation () {}, 'keyCode': 13 // Enter } spyOn(_converse.connection, 'send'); view.onKeyDown(enter_event); await new Promise(resolve => view.once('messageInserted', resolve)); const msg = _converse.connection.send.calls.all()[0].args[0]; expect(msg.toLocaleString()) .toBe(``+ `hello Link Mauve`+ ``+ ``+ ``+ ``); done(); })); it("can get corrected and given new references", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'tom'); const view = _converse.api.chatviews.get(muc_jid); ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].forEach((nick) => { _converse.connection._dataRecv(mock.createRequest( $pres({ 'to': 'tom@montague.lit/resource', 'from': `lounge@montague.lit/${nick}` }) .c('x', {xmlns: Strophe.NS.MUC_USER}) .c('item', { 'affiliation': 'none', 'jid': `${nick}@montague.lit/resource`, 'role': 'participant' }))); }); await u.waitUntil(() => view.model.occupants.length === 5); const textarea = view.el.querySelector('textarea.chat-textarea'); textarea.value = 'hello @z3r0 @gibson @mr.robot, how are you?' const enter_event = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'stopPropagation': function stopPropagation () {}, 'keyCode': 13 // Enter } spyOn(_converse.connection, 'send'); view.onKeyDown(enter_event); await new Promise(resolve => view.once('messageInserted', resolve)); const msg = _converse.connection.send.calls.all()[0].args[0]; expect(msg.toLocaleString()) .toBe(``+ `hello z3r0 gibson mr.robot, how are you?`+ ``+ ``+ ``+ ``+ ``+ ``); const action = view.el.querySelector('.chat-msg .chat-msg__action'); action.style.opacity = 1; action.click(); expect(textarea.value).toBe('hello @z3r0 @gibson @mr.robot, how are you?'); expect(view.model.messages.at(0).get('correcting')).toBe(true); expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500); await u.waitUntil(() => _converse.connection.send.calls.count() === 2); textarea.value = 'hello @z3r0 @gibson @sw0rdf1sh, how are you?'; view.onKeyDown(enter_event); await u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent === 'hello z3r0 gibson sw0rdf1sh, how are you?', 500); const correction = _converse.connection.send.calls.all()[2].args[0]; expect(correction.toLocaleString()) .toBe(``+ `hello z3r0 gibson sw0rdf1sh, how are you?`+ ``+ ``+ ``+ ``+ ``+ ``+ ``); done(); })); it("includes XEP-0372 references to that person", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const view = _converse.api.chatviews.get(muc_jid); ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].forEach((nick) => { _converse.connection._dataRecv(mock.createRequest( $pres({ 'to': 'tom@montague.lit/resource', 'from': `lounge@montague.lit/${nick}` }) .c('x', {xmlns: Strophe.NS.MUC_USER}) .c('item', { 'affiliation': 'none', 'jid': `${nick}@montague.lit/resource`, 'role': 'participant' }))); }); await u.waitUntil(() => view.model.occupants.length === 5); spyOn(_converse.connection, 'send'); const textarea = view.el.querySelector('textarea.chat-textarea'); textarea.value = 'hello @z3r0 @gibson @mr.robot, how are you?' const enter_event = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'stopPropagation': function stopPropagation () {}, 'keyCode': 13 // Enter } view.onKeyDown(enter_event); await new Promise(resolve => view.once('messageInserted', resolve)); const msg = _converse.connection.send.calls.all()[0].args[0]; expect(msg.toLocaleString()) .toBe(``+ `hello z3r0 gibson mr.robot, how are you?`+ ``+ ``+ ``+ ``+ ``+ ``); done(); })); }); });