/*global mock, converse */ const { Promise, Strophe, $msg, $pres, sizzle, stanza_utils } = converse.env; const u = converse.env.utils; const original_timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; describe("A Groupchat Message", function () { beforeEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000)); afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout)); 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.model.messages.once('rendered', 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_text')).toBe(err_msg_text); expect(message.get('editable')).toBe(false); 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.model.messages.once('rendered', resolve)); expect(view.el.querySelector('.chat-msg')).not.toBe(null); 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': 'owner', '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] }); await u.waitUntil(() => converse.env.log.error.calls.count()); 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 chat-msg--with-avatar 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.model.messages.once('rendered', 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 chat-msg--with-avatar 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 chat-msg--with-avatar 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.model.messages.once('rendered', 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); await u.waitUntil(() => view.el.querySelector('.chat-msg__content .fa-edit')); 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); const edit = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .fa-edit')); edit.click(); const modal = await u.waitUntil(() => view.el.querySelector('converse-chat-message').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("keeps the same position in history after 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(); // Receiving the first message 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()); // Receiving own message to check order against await view.model.handleMessageStanza($msg({ 'from': 'lounge@montague.lit/romeo', '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 === 2); expect(view.el.querySelectorAll('.chat-msg').length).toBe(2); expect(view.el.querySelectorAll('.chat-msg__text')[0].textContent) .toBe('But soft, what light through yonder airlock breaks?'); expect(view.el.querySelectorAll('.chat-msg__text')[1].textContent) .toBe('But soft, what light through yonder airlock breaks?'); // First message correction 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(2); await u.waitUntil(() => view.el.querySelector('.chat-msg__content .fa-edit')); // Second message correction 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()); // Second own message await view.model.handleMessageStanza($msg({ 'from': 'lounge@montague.lit/romeo', 'to': _converse.connection.jid, 'type': 'groupchat', 'id': u.getUniqueId(), }).c('body').t('But soft, what light through yonder window breaks?').tree()); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__text')[0].textContent === 'But soft, what light through yonder window breaks?', 500); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__text').length === 3); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__text')[2].textContent === 'But soft, what light through yonder window breaks?', 500); expect(view.el.querySelectorAll('.chat-msg').length).toBe(3); expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1); const edit = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .fa-edit')); edit.click(); const modal = await u.waitUntil(() => view.el.querySelectorAll('converse-chat-message')[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 }); 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); await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg'))); 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.model.messages.once('rendered', 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.model.messages.once('rendered', 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.model.messages.once('rendered', 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.model.messages.once('rendered', 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(); })); });