UNPKG

converse.js

Version:
937 lines (816 loc) 74.2 kB
/*global mock, converse */ const { Strophe, $msg, dayjs, sizzle, stx, u } = converse.env; describe("A Chat Message", function () { it("will be demarcated if it's the first newly received message", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { await mock.waitForRoster(_converse, 'current', 1); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await mock.openChatBoxFor(_converse, contact_jid); const view = _converse.chatboxviews.get(contact_jid); await _converse.handleMessageStanza(mock.createChatMessage(_converse, contact_jid, 'This message will be read')); await u.waitUntil(() => view.querySelector('converse-chat-message .chat-msg__text')?.textContent === 'This message will be read'); expect(view.model.get('num_unread')).toBe(0); spyOn(view.model, 'isHidden').and.returnValue(true); await _converse.handleMessageStanza(mock.createChatMessage(_converse, contact_jid, 'This message will be new')); await u.waitUntil(() => view.model.messages.length); expect(view.model.get('num_unread')).toBe(1); expect(view.model.get('first_unread_id')).toBe(view.model.messages.last().get('id')); await u.waitUntil(() => view.querySelectorAll('converse-chat-message').length === 2); await u.waitUntil(() => view.querySelector('converse-chat-message:last-child .chat-msg__text')?.textContent === 'This message will be new'); const last_msg_el = view.querySelector('converse-chat-message:last-child'); expect(last_msg_el.firstElementChild?.textContent).toBe('New messages'); })); it("is rejected if it's an unencapsulated forwarded message", mock.initConverse( ['chatBoxesFetched'], {}, async function (_converse) { const { api } = _converse; await mock.waitForRoster(_converse, 'current', 2); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const forwarded_contact_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await mock.openChatBoxFor(_converse, contact_jid); let models = await _converse.api.chats.get(); expect(models.length).toBe(1); const received_stanza = stx` <message xmlns="jabber:client" to='${_converse.jid}' from='${contact_jid}' type='chat' id='${api.connection.get().getUniqueId()}'> <body>A most courteous exposition!</body> <forwarded xmlns='urn:xmpp:forward:0'> <delay xmlns='urn:xmpp:delay' stamp='2019-07-10T23:08:25Z'/> <message from='${forwarded_contact_jid}' id='0202197' to='${_converse.bare_jid}' type='chat' xmlns='jabber:client'> <body>Yet I should kill thee with much cherishing.</body> <mood xmlns='http://jabber.org/protocol/mood'> <amorous/> </mood> </message> </forwarded> </message>`; api.connection.get()._dataRecv(mock.createRequest(received_stanza)); const sent_stanzas = api.connection.get().sent_stanzas; const sent_stanza = await u.waitUntil(() => sent_stanzas.filter(s => s.querySelector('error')).pop()); expect(Strophe.serialize(sent_stanza)).toBe( `<message id="${received_stanza.tree().getAttribute('id')}" to="${contact_jid}" type="error" xmlns="jabber:client">`+ '<error type="cancel">'+ '<not-allowed xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>'+ '<text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">'+ 'Forwarded messages not part of an encapsulating protocol are not supported</text>'+ '</error>'+ '</message>'); models = await _converse.api.chats.get(); expect(models.length).toBe(1); })); it("can be received out of order, and will still be displayed in the right order", mock.initConverse([], {}, async function (_converse) { const { api } = _converse; await mock.waitForRoster(_converse, 'current'); await mock.openControlBox(_converse); const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const rosterview = document.querySelector('converse-roster'); await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length) api.settings.set('filter_by_resource', true); let msg = stx` <message xmlns="jabber:client" id="${api.connection.get().getUniqueId()}" to="${_converse.bare_jid}" from="${sender_jid}" type="chat"> <body>message</body> <delay xmlns="urn:xmpp:delay" stamp="2018-01-02T13:08:25Z"/> </message>`; await _converse.handleMessageStanza(msg); const view = _converse.chatboxviews.get(sender_jid); msg = stx` <message xmlns="jabber:client" id="${api.connection.get().getUniqueId()}" to="${_converse.bare_jid}" from="${sender_jid}" type="chat"> <body>Older message</body> <delay xmlns="urn:xmpp:delay" stamp="2017-12-31T11:08:25Z"/> </message>`; _converse.handleMessageStanza(msg); await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 2); msg = stx` <message xmlns="jabber:client" id="${api.connection.get().getUniqueId()}" to="${_converse.bare_jid}" from="${sender_jid}" type="chat"> <body>Inbetween message</body> <delay xmlns="urn:xmpp:delay" stamp="2018-01-01T13:18:23Z"/> </message>`; _converse.handleMessageStanza(msg); await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 3); msg = stx` <message xmlns="jabber:client" id="${api.connection.get().getUniqueId()}" to="${_converse.bare_jid}" from="${sender_jid}" type="chat"> <body>another inbetween message</body> <delay xmlns="urn:xmpp:delay" stamp="2018-01-01T13:18:23Z"/> </message>`; _converse.handleMessageStanza(msg); await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 4); msg = stx` <message xmlns="jabber:client" id="${api.connection.get().getUniqueId()}" to="${_converse.bare_jid}" from="${sender_jid}" type="chat"> <body>An earlier message on the next day</body> <delay xmlns="urn:xmpp:delay" stamp="2018-01-02T12:18:23Z"/> </message>`; _converse.handleMessageStanza(msg); await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 5); msg = stx` <message xmlns="jabber:client" id="${api.connection.get().getUniqueId()}" to="${_converse.bare_jid}" from="${sender_jid}" type="chat"> <body>newer message from the next day</body> <delay xmlns="urn:xmpp:delay" stamp="2018-01-02T20:28:23Z"/> </message>`; _converse.handleMessageStanza(msg); await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 6); // Insert <composing> message, to also check that // text messages are inserted correctly with // temporary chat events in the chat contents. msg = stx` <message xmlns="jabber:client" id="${api.connection.get().getUniqueId()}" to="${_converse.bare_jid}" from="${sender_jid}" type="chat"> <composing xmlns="${Strophe.NS.CHATSTATES}"/> </message>`; _converse.handleMessageStanza(msg); const csntext = await u.waitUntil(() => view.querySelector('.chat-content__notifications').textContent); expect(csntext.trim()).toEqual('Mercutio is typing'); msg = stx` <message xmlns="jabber:client" id="${api.connection.get().getUniqueId()}" to="${_converse.bare_jid}" from="${sender_jid}" type="chat"> <composing xmlns="${Strophe.NS.CHATSTATES}"/> <body>latest message</body> </message>`; await _converse.handleMessageStanza(msg); await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 7); expect(view.querySelectorAll('.date-separator').length).toEqual(4); let day = sizzle('.date-separator:first', view).pop(); expect(day.getAttribute('data-isodate')).toEqual(dayjs('2017-12-31T00:00:00').toISOString()); let time = sizzle('time:first', view).pop(); expect(time.textContent).toEqual('Sunday Dec 31st 2017') day = sizzle('.date-separator:first', view).pop(); expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('Older message'); let el = sizzle('.chat-msg:first', view).pop().querySelector('.chat-msg__text') expect(u.hasClass('chat-msg--followup', el)).toBe(false); expect(el.textContent).toEqual('Older message'); time = sizzle('time.separator-text:eq(1)', view).pop(); expect(time.textContent).toEqual("Monday Jan 1st 2018"); day = sizzle('.date-separator:eq(1)', view).pop(); expect(day.getAttribute('data-isodate')).toEqual(dayjs('2018-01-01T00:00:00').toISOString()); expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('Inbetween message'); el = sizzle('.chat-msg:eq(1)', view).pop(); expect(el.querySelector('.chat-msg__text').textContent).toEqual('Inbetween message'); expect(el.parentElement.nextElementSibling.querySelector('.chat-msg__text').textContent).toEqual('another inbetween message'); el = sizzle('.chat-msg:eq(2)', view).pop(); expect(el.querySelector('.chat-msg__text').textContent) .toEqual('another inbetween message'); expect(u.hasClass('chat-msg--followup', el)).toBe(true); time = sizzle('time.separator-text:nth(2)', view).pop(); expect(time.textContent).toEqual("Tuesday Jan 2nd 2018"); day = sizzle('.date-separator:nth(2)', view).pop(); expect(day.getAttribute('data-isodate')).toEqual(dayjs('2018-01-02T00:00:00').toISOString()); expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('An earlier message on the next day'); el = sizzle('.chat-msg:eq(3)', view).pop(); expect(el.querySelector('.chat-msg__text').textContent).toEqual('An earlier message on the next day'); expect(u.hasClass('chat-msg--followup', el)).toBe(false); el = sizzle('.chat-msg:eq(4)', view).pop(); expect(el.querySelector('.chat-msg__text').textContent).toEqual('message'); expect(el.parentElement.nextElementSibling.querySelector('.chat-msg__text').textContent).toEqual('newer message from the next day'); expect(u.hasClass('chat-msg--followup', el)).toBe(false); day = sizzle('.date-separator:last', view).pop(); expect(day.getAttribute('data-isodate')).toEqual(dayjs().startOf('day').toISOString()); expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('latest message'); expect(u.hasClass('chat-msg--followup', el)).toBe(false); })); it("is ignored if it's a malformed headline message", mock.initConverse([], {}, async function (_converse) { await mock.waitForRoster(_converse, 'current'); await mock.openControlBox(_converse); // Ideally we wouldn't have to filter out headline // messages, but Prosody gives them the wrong 'type' :( spyOn(converse.env.log, 'info'); spyOn(_converse.api.chatboxes, 'get'); const msg = stx` <message xmlns="jabber:client" from="montague.lit" to="${_converse.bare_jid}" type="chat" id="${u.getUniqueId()}"> <body>This headline message will not be shown</body> </message>`; await _converse.handleMessageStanza(msg); expect(converse.env.log.info).toHaveBeenCalledWith( "handleMessageStanza: Ignoring incoming server message from JID: montague.lit" ); expect(_converse.api.chatboxes.get).not.toHaveBeenCalled(); })); it("will render Openstreetmap-URL from geo-URI", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { await mock.waitForRoster(_converse, 'current', 1); const message = "geo:37.786971,-122.399677"; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await mock.openChatBoxFor(_converse, contact_jid); const view = _converse.chatboxviews.get(contact_jid); spyOn(view.model, 'sendMessage').and.callThrough(); await mock.sendMessage(view, message); await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg').length, 1000); expect(view.model.sendMessage).toHaveBeenCalled(); const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop(); await u.waitUntil(() => msg.innerHTML.replace(/\<!-.*?-\>/g, '') === '<a target="_blank" rel="noopener" href="https://www.openstreetmap.org/?mlat=37.786971&amp;'+ 'mlon=-122.399677#map=18/37.786971/-122.399677">https://www.openstreetmap.org/?mlat=37.786971&amp;mlon=-122.399677#map=18/37.786971/-122.399677</a>'); })); it("can be a carbon message, as defined in XEP-0280", mock.initConverse([], {}, async function (_converse) { const { api } = _converse; const include_nick = false; await mock.waitForRoster(_converse, 'current', 2, include_nick); await mock.openControlBox(_converse); // Send a message from a different resource const msgtext = 'This is a carbon message'; const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const msg = $msg({ 'from': _converse.bare_jid, 'id': u.getUniqueId(), 'to': api.connection.get().jid, 'type': 'chat', 'xmlns': 'jabber:client' }).c('received', {'xmlns': 'urn:xmpp:carbons:2'}) .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) .c('message', { 'xmlns': 'jabber:client', 'from': sender_jid, 'to': _converse.bare_jid+'/another-resource', 'type': 'chat' }).c('body').t(msgtext).tree(); await _converse.handleMessageStanza(msg); const chatbox = _converse.chatboxes.get(sender_jid); const view = _converse.chatboxviews.get(sender_jid); expect(chatbox).toBeDefined(); expect(view).toBeDefined(); // Check that the message was received and check the message parameters await u.waitUntil(() => chatbox.messages.length); const msg_obj = chatbox.messages.models[0]; expect(msg_obj.get('message')).toEqual(msgtext); expect(msg_obj.get('fullname')).toBeUndefined(); expect(msg_obj.get('nickname')).toBe(null); expect(msg_obj.get('sender')).toEqual('them'); expect(msg_obj.get('is_delayed')).toEqual(false); // Now check that the message appears inside the chatbox in the DOM await u.waitUntil(() => view.querySelector('.chat-msg .chat-msg__text')); expect(view.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(msgtext); expect(view.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy(); await u.waitUntil(() => chatbox.vcard.get('fullname') === 'Juliet Capulet'); expect(view.querySelector('.chatbox-title__text .show-msg-author-modal').textContent.trim()).toBe('Juliet Capulet'); await u.waitUntil(() => view.querySelector('span.chat-msg__author').textContent.trim() === 'Juliet Capulet'); })); it("can be a carbon message that this user sent from a different client, as defined in XEP-0280", mock.initConverse([], {}, async function (_converse) { const { api } = _converse; await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']); await mock.waitForRoster(_converse, 'current'); await mock.openControlBox(_converse); // Send a message from a different resource const msgtext = 'This is a sent carbon message'; const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const msg = $msg({ 'from': _converse.bare_jid, 'id': u.getUniqueId(), 'to': api.connection.get().jid, 'type': 'chat', 'xmlns': 'jabber:client' }).c('sent', {'xmlns': 'urn:xmpp:carbons:2'}) .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) .c('message', { 'xmlns': 'jabber:client', 'from': _converse.bare_jid+'/another-resource', 'to': recipient_jid, 'type': 'chat' }).c('body').t(msgtext).tree(); await _converse.handleMessageStanza(msg); // Check that the chatbox and its view now exist const chatbox = await _converse.api.chats.get(recipient_jid); const view = _converse.chatboxviews.get(recipient_jid); expect(chatbox).toBeDefined(); expect(view).toBeDefined(); // Check that the message was received and check the message parameters expect(chatbox.messages.length).toEqual(1); const msg_obj = chatbox.messages.models[0]; expect(msg_obj.get('message')).toEqual(msgtext); expect(msg_obj.get('fullname')).toEqual(_converse.xmppstatus.get('fullname')); expect(msg_obj.get('sender')).toEqual('me'); expect(msg_obj.get('is_delayed')).toEqual(false); // Now check that the message appears inside the chatbox in the DOM const msg_el = await u.waitUntil(() => view.querySelector('.chat-content .chat-msg .chat-msg__text')); expect(msg_el.textContent).toEqual(msgtext); })); it("will be discarded if it's a malicious message meant to look like a carbon copy", mock.initConverse([], {}, async function (_converse) { const { api } = _converse; await mock.waitForRoster(_converse, 'current'); await mock.openControlBox(_converse); /* <message from="mallory@evil.example" to="b@xmpp.example"> * <received xmlns='urn:xmpp:carbons:2'> * <forwarded xmlns='urn:xmpp:forward:0'> * <message from="alice@xmpp.example" to="bob@xmpp.example/client1"> * <body>Please come to Creepy Valley tonight, alone!</body> * </message> * </forwarded> * </received> * </message> */ const msgtext = 'Please come to Creepy Valley tonight, alone!'; const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const impersonated_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const msg = $msg({ 'from': sender_jid, 'id': u.getUniqueId(), 'to': api.connection.get().jid, 'type': 'chat', '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': api.connection.get().jid, 'type': 'chat' }).c('body').t(msgtext).tree(); await _converse.handleMessageStanza(msg); // Check that chatbox for impersonated user is not created. let chatbox = await _converse.api.chats.get(impersonated_jid); expect(chatbox).toBe(null); // Check that the chatbox for the malicous user is not created chatbox = await _converse.api.chats.get(sender_jid); expect(chatbox).toBe(null); })); it("will indicate when it has a time difference of more than a day between it and its predecessor", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { const { api } = _converse; const include_nick = false; await mock.waitForRoster(_converse, 'current', 2, include_nick); await mock.openControlBox(_converse); spyOn(_converse.api, "trigger").and.callThrough(); const contact_name = mock.cur_names[1]; const contact_jid = contact_name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; const rosterview = document.querySelector('converse-roster'); await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length); await mock.openChatBoxFor(_converse, contact_jid); const one_day_ago = dayjs().subtract(1, 'day'); const chatbox = _converse.chatboxes.get(contact_jid); const view = _converse.chatboxviews.get(contact_jid); let message = 'This is a day old message'; let msg = $msg({ from: contact_jid, to: api.connection.get().jid, type: 'chat', id: one_day_ago.toDate().getTime() }).c('body').t(message).up() .c('delay', { xmlns:'urn:xmpp:delay', from: 'montague.lit', stamp: one_day_ago.toISOString() }) .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); await _converse.handleMessageStanza(msg); await u.waitUntil(() => view.querySelectorAll('.chat-msg').length); expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); expect(chatbox.messages.length).toEqual(1); let msg_obj = chatbox.messages.models[0]; expect(msg_obj.get('message')).toEqual(message); expect(msg_obj.get('fullname')).toBeUndefined(); expect(msg_obj.get('nickname')).toBe(null); expect(msg_obj.get('sender')).toEqual('them'); expect(msg_obj.get('is_delayed')).toEqual(true); await u.waitUntil(() => chatbox.vcard.get('fullname') === 'Juliet Capulet') expect(view.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message); expect(view.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy(); await u.waitUntil(() => view.querySelector('span.chat-msg__author').textContent.trim() === 'Juliet Capulet'); expect(view.querySelectorAll('.date-separator').length).toEqual(1); let day = view.querySelector('.date-separator'); expect(day.getAttribute('class')).toEqual('message date-separator'); expect(day.getAttribute('data-isodate')).toEqual(dayjs(one_day_ago.startOf('day')).toISOString()); let time = view.querySelector('time.separator-text'); expect(time.textContent).toEqual(dayjs(one_day_ago.startOf('day')).format("dddd MMM Do YYYY")); message = 'This is a current message'; msg = $msg({ from: contact_jid, to: api.connection.get().jid, type: 'chat', id: new Date().getTime() }).c('body').t(message).up() .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); await _converse.handleMessageStanza(msg); await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 2); expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); // Check that there is a <time> element, with the required props. expect(view.querySelectorAll('time.separator-text').length).toEqual(2); // There are now two time elements const message_date = new Date(); day = sizzle('.date-separator:last', view); expect(day.length).toEqual(1); expect(day[0].getAttribute('class')).toEqual('message date-separator'); expect(day[0].getAttribute('data-isodate')).toEqual(dayjs(message_date).startOf('day').toISOString()); time = sizzle('time.separator-text:last', view).pop(); expect(time.textContent).toEqual(dayjs(message_date).startOf('day').format("dddd MMM Do YYYY")); // Normal checks for the 2nd message expect(chatbox.messages.length).toEqual(2); msg_obj = chatbox.messages.models[1]; expect(msg_obj.get('message')).toEqual(message); expect(msg_obj.get('fullname')).toBeUndefined(); expect(msg_obj.get('sender')).toEqual('them'); expect(msg_obj.get('is_delayed')).toEqual(false); const msg_txt = sizzle('.chat-msg:last .chat-msg__text', view).pop().textContent; expect(msg_txt).toEqual(message); expect(view.querySelector('converse-chat-message:last-child .chat-msg__text').textContent).toEqual(message); expect(view.querySelector('converse-chat-message:last-child .chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy(); expect(view.querySelector('converse-chat-message:last-child .chat-msg__author').textContent.trim()).toBe('Juliet Capulet'); })); it("is sanitized to prevent Javascript injection attacks", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { await mock.waitForRoster(_converse, 'current'); await mock.openControlBox(_converse); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await mock.openChatBoxFor(_converse, contact_jid) const view = _converse.chatboxviews.get(contact_jid); const message = '<p>This message contains <em>some</em> <b>markup</b></p>'; spyOn(view.model, 'sendMessage').and.callThrough(); await mock.sendMessage(view, message); expect(view.model.sendMessage).toHaveBeenCalled(); const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop(); expect(msg.textContent).toEqual(message); expect(msg.innerHTML.replace(/<!-.*?->/g, '')).toEqual('&lt;p&gt;This message contains &lt;em&gt;some&lt;/em&gt; &lt;b&gt;markup&lt;/b&gt;&lt;/p&gt;'); })); it("can contain hyperlinks, which will be clickable", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { await mock.waitForRoster(_converse, 'current'); await mock.openControlBox(_converse); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await mock.openChatBoxFor(_converse, contact_jid) const view = _converse.chatboxviews.get(contact_jid); const message = 'This message contains a hyperlink: www.opkode.com'; spyOn(view.model, 'sendMessage').and.callThrough(); await mock.sendMessage(view, message); expect(view.model.sendMessage).toHaveBeenCalled(); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length); const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop(); expect(msg.textContent).toEqual(message); await u.waitUntil(() => msg.innerHTML.replace(/<!-.*?->/g, '') === 'This message contains a hyperlink: <a target="_blank" rel="noopener" href="http://www.opkode.com/">www.opkode.com</a>'); })); it("will remove url query parameters from hyperlinks as set", mock.initConverse(['chatBoxesFetched'], { filter_url_query_params: ['utm_medium', 'utm_content', 's']}, async function (_converse) { const originalFetch = window.fetch; spyOn(window, 'fetch').and.callFake(async (...args) => { if (args[1].method === 'HEAD') { return new Response('', { status: 200, headers: { 'Content-Type': 'text/html' } }); } return await originalFetch(...args); }); await mock.waitForRoster(_converse, 'current'); await mock.openControlBox(_converse); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await mock.openChatBoxFor(_converse, contact_jid); const view = _converse.chatboxviews.get(contact_jid); let message = 'This message contains a hyperlink with forbidden query params: https://www.opkode.com/?id=0&utm_content=1&utm_medium=2&s=1'; await mock.sendMessage(view, message); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length); let msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop(); await u.waitUntil(() => msg.innerHTML.replace(/<!-.*?->/g, '') === 'This message contains a hyperlink with forbidden query params: <a target="_blank" rel="noopener" href="https://www.opkode.com/?id=0">https://www.opkode.com/?id=0</a>'); // Test assigning a string to filter_url_query_params _converse.api.settings.set('filter_url_query_params', 'utm_medium'); message = 'Another message with a hyperlink with forbidden query params: https://www.opkode.com/?id=0&utm_content=1&utm_medium=2&s=1'; await mock.sendMessage(view, message); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 2); msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop(); expect(msg.textContent).toEqual(message); await u.waitUntil(() => msg.innerHTML.replace(/<!-.*?->/g, '') === 'Another message with a hyperlink with forbidden query params: '+ '<a target="_blank" rel="noopener" href="https://www.opkode.com/?id=0&amp;utm_content=1&amp;s=1">https://www.opkode.com/?id=0&amp;utm_content=1&amp;s=1</a>'); })); it("properly renders URLs", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { const originalFetch = window.fetch; spyOn(window, 'fetch').and.callFake(async (...args) => { if (args[1].method === 'HEAD') { return new Response('', { status: 200, headers: { 'Content-Type': 'text/html' } }); } return await originalFetch(...args); }); await mock.waitForRoster(_converse, 'current'); await mock.openControlBox(_converse); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await mock.openChatBoxFor(_converse, contact_jid); const view = _converse.chatboxviews.get(contact_jid); const message = 'https://mov.im/?node/pubsub.movim.eu/Dino/urn-uuid-979bd24f-0bf3-5099-9fa7-510b9ce9a884'; await mock.sendMessage(view, message); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length); const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop(); const anchor = await u.waitUntil(() => msg.querySelector('a')); expect(anchor.innerHTML.replace(/<!-.*?->/g, '')).toBe(message); expect(anchor.getAttribute('href')).toBe(message); })); it("will render newlines", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { const { api } = _converse; await mock.waitForRoster(_converse, 'current'); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const view = await mock.openChatBoxFor(_converse, contact_jid); let stanza = stx` <message from="${contact_jid}" type="chat" to="romeo@montague.lit/orchard" xmlns="jabber:client"> <body>Hey\nHave you heard the news?</body> </message>`; api.connection.get()._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length); expect(view.querySelector('.chat-msg__text').innerHTML.replace(/<!-.*?->/g, '')).toBe('Hey\nHave you heard the news?'); stanza = stx` <message from="${contact_jid}" type="chat" to="romeo@montague.lit/orchard" xmlns="jabber:client"> <body>Hey\n\n\nHave you heard the news?</body> </message>`; api.connection.get()._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 2); const text = view.querySelector('converse-chat-message:last-child .chat-msg__text').innerHTML.replace(/<!-.*?->/g, ''); expect(text).toBe('Hey\n\u200B\nHave you heard the news?'); stanza = stx` <message from="${contact_jid}" type="chat" to="romeo@montague.lit/orchard" xmlns="jabber:client"> <body>Hey\nHave you heard\nthe news?</body> </message>`; api.connection.get()._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 3); expect(view.querySelector('converse-chat-message:last-child .chat-msg__text').innerHTML.replace(/<!-.*?->/g, '')).toBe('Hey\nHave you heard\nthe news?'); stanza = stx` <message from="${contact_jid}" type="chat" to="romeo@montague.lit/orchard" xmlns="jabber:client"> <body>Hey\nHave you heard\n\n\nthe news?\nhttps://conversejs.org</body> </message>`; api.connection.get()._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 4); await u.waitUntil(() => { const text = view.querySelector('converse-chat-message:last-child .chat-msg__text').innerHTML.replace(/<!-.*?->/g, ''); return text === 'Hey\nHave you heard\n\u200B\nthe news?\n<a target="_blank" rel="noopener" href="https://conversejs.org/">https://conversejs.org</a>'; }); })); it("will render the message time as configured", mock.initConverse( ['chatBoxesFetched'], {}, async function (_converse) { const { api } = _converse; await mock.waitForRoster(_converse, 'current'); api.settings.set('time_format', 'hh:mm'); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await mock.openChatBoxFor(_converse, contact_jid) const view = _converse.chatboxviews.get(contact_jid); const message = 'This message is sent from this chatbox'; await mock.sendMessage(view, message); const chatbox = await _converse.api.chats.get(contact_jid); expect(chatbox.messages.models.length, 1); const msg_object = chatbox.messages.models[0]; const msg_author = view.querySelector('.chat-content .chat-msg:last-child .chat-msg__author'); expect(msg_author.textContent.trim()).toBe('Romeo Montague'); const msg_time = view.querySelector('.chat-content .chat-msg:last-child .chat-msg__time'); const time = dayjs(msg_object.get('time')).format(api.settings.get('time_format')); expect(msg_time.textContent).toBe(time); })); it("will be correctly identified and rendered as a followup message", mock.initConverse( [], {'debounced_content_rendering': false}, async function (_converse) { const { api } = _converse; await mock.waitForRoster(_converse, 'current'); await mock.openControlBox(_converse); const base_time = new Date(); const ONE_MINUTE_LATER = 60000; const rosterview = document.querySelector('converse-roster'); await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length, 300); const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; api.settings.set('filter_by_resource', true); jasmine.clock().install(); jasmine.clock().mockDate(base_time); _converse.handleMessageStanza($msg({ 'from': sender_jid, 'to': api.connection.get().jid, 'type': 'chat', 'id': u.getUniqueId() }).c('body').t('A message').up() .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); await new Promise(resolve => _converse.on('chatBoxViewInitialized', resolve)); const view = _converse.chatboxviews.get(sender_jid); await new Promise(resolve => view.model.messages.once('rendered', resolve)); jasmine.clock().tick(3*ONE_MINUTE_LATER); _converse.handleMessageStanza($msg({ 'from': sender_jid, 'to': api.connection.get().jid, 'type': 'chat', 'id': u.getUniqueId() }).c('body').t("Another message 3 minutes later").up() .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); await new Promise(resolve => view.model.messages.once('rendered', resolve)); jasmine.clock().tick(11*ONE_MINUTE_LATER); _converse.handleMessageStanza($msg({ 'from': sender_jid, 'to': api.connection.get().jid, 'type': 'chat', 'id': u.getUniqueId() }).c('body').t("Another message 14 minutes since we started").up() .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); await new Promise(resolve => view.model.messages.once('rendered', resolve)); jasmine.clock().tick(1*ONE_MINUTE_LATER); _converse.handleMessageStanza($msg({ 'from': sender_jid, 'to': api.connection.get().jid, 'type': 'chat', 'id': api.connection.get().getUniqueId() }).c('body').t("Another message 1 minute and 1 second since the previous one").up() .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); await new Promise(resolve => view.model.messages.once('rendered', resolve)); jasmine.clock().tick(1*ONE_MINUTE_LATER); await mock.sendMessage(view, "Another message within 10 minutes, but from a different person"); await u.waitUntil(() => view.querySelectorAll('.message').length === 6); expect(view.querySelectorAll('.chat-msg').length).toBe(5); const nth_child = (n) => `converse-chat-message:nth-child(${n}) .chat-msg`; expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(2)))).toBe(false); expect(view.querySelector(`${nth_child(2)} .chat-msg__text`).textContent).toBe("A message"); expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(3)))).toBe(true); expect(view.querySelector(`${nth_child(3)} .chat-msg__text`).textContent).toBe( "Another message 3 minutes later"); expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(4)))).toBe(false); expect(view.querySelector(`${nth_child(4)} .chat-msg__text`).textContent).toBe( "Another message 14 minutes since we started"); expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(5)))).toBe(true); expect(view.querySelector(`${nth_child(5)} .chat-msg__text`).textContent).toBe( "Another message 1 minute and 1 second since the previous one"); expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(6)))).toBe(false); expect(view.querySelector(`${nth_child(6)} .chat-msg__text`).textContent).toBe( "Another message within 10 minutes, but from a different person"); // Let's add a delayed, inbetween message _converse.handleMessageStanza( $msg({ 'xmlns': 'jabber:client', 'id': api.connection.get().getUniqueId(), 'to': _converse.bare_jid, 'from': sender_jid, 'type': 'chat' }).c('body').t("A delayed message, sent 5 minutes since we started").up() .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp': dayjs(base_time).add(5, 'minutes').toISOString()}) .tree()); await new Promise(resolve => view.model.messages.once('rendered', resolve)); expect(view.querySelectorAll('.message').length).toBe(7); expect(view.querySelectorAll('.chat-msg').length).toBe(6); expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(2)))).toBe(false); expect(view.querySelector(`${nth_child(2)} .chat-msg__text`).textContent).toBe("A message"); expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(3)))).toBe(true); expect(view.querySelector(`${nth_child(3)} .chat-msg__text`).textContent).toBe( "Another message 3 minutes later"); expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(4)))).toBe(true); expect(view.querySelector(`${nth_child(4)} .chat-msg__text`).textContent).toBe( "A delayed message, sent 5 minutes since we started"); expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(5)))).toBe(false); expect(view.querySelector(`${nth_child(5)} .chat-msg__text`).textContent).toBe( "Another message 14 minutes since we started"); expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(6)))).toBe(true); expect(view.querySelector(`${nth_child(6)} .chat-msg__text`).textContent).toBe( "Another message 1 minute and 1 second since the previous one"); expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(7)))).toBe(false); expect(view.querySelector(`${nth_child(7)} .chat-msg__text`).textContent).toBe( "Another message within 10 minutes, but from a different person"); _converse.handleMessageStanza( $msg({ 'xmlns': 'jabber:client', 'id': api.connection.get().getUniqueId(), 'to': sender_jid, 'from': _converse.bare_jid+"/some-other-resource", 'type': 'chat'}) .c('body').t("A carbon message 4 minutes later").up() .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':dayjs(base_time).add(4, 'minutes').toISOString()}) .tree()); await new Promise(resolve => view.model.messages.once('rendered', resolve)); expect(view.querySelectorAll('.chat-msg').length).toBe(7); expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(2)))).toBe(false); expect(view.querySelector(`${nth_child(2)} .chat-msg__text`).textContent).toBe("A message"); expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(3)))).toBe(true); expect(view.querySelector(`${nth_child(3)} .chat-msg__text`).textContent).toBe( "Another message 3 minutes later"); expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(4)))).toBe(false); expect(view.querySelector(`${nth_child(4)} .chat-msg__text`).textContent).toBe( "A carbon message 4 minutes later"); expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(5)))).toBe(true); expect(view.querySelector(`${nth_child(5)} .chat-msg__text`).textContent).toBe( "A delayed message, sent 5 minutes since we started"); expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(6)))).toBe(false); expect(view.querySelector(`${nth_child(6)} .chat-msg__text`).textContent).toBe( "Another message 14 minutes since we started"); expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(7)))).toBe(true); expect(view.querySelector(`${nth_child(7)} .chat-msg__text`).textContent).toBe( "Another message 1 minute and 1 second since the previous one"); expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(8)))).toBe(false); expect(view.querySelector(`${nth_child(8)} .chat-msg__text`).textContent).toBe( "Another message within 10 minutes, but from a different person"); jasmine.clock().uninstall(); })); describe("when sent", function () { it("will appear inside the chatbox it was sent from", mock.initConverse( ['chatBoxesFetched'], {}, async function (_converse) { await mock.waitForRoster(_converse, 'current'); await mock.openControlBox(_converse); spyOn(_converse.api, "trigger").and.callThrough(); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await mock.openChatBoxFor(_converse, contact_jid) const view = _converse.chatboxviews.get(contact_jid); const message = 'This message is sent from this chatbox'; spyOn(view.model, 'sendMessage').and.callThrough(); await mock.sendMessage(view, message); expect(view.model.sendMessage).toHaveBeenCalled(); expect(view.model.messages.length, 2); expect(sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop().textContent).toEqual(message); })); it("will be trimmed of leading and trailing whitespace", mock.initConverse( ['chatBoxesFetched'], {}, async function (_converse) { await mock.waitForRoster(_converse, 'current', 1); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await mock.openChatBoxFor(_converse, contact_jid) const view = _converse.chatboxviews.get(contact_jid); const message = ' \nThis message is sent from this chatbox \n \n'; await mock.sendMessage(view, message); expect(view.model.messages.at(0).get('message')).toEqual(message.trim()); const message_el = sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop(); expect(message_el.textContent).toEqual(message.trim()); })); }); describe("when received from someone else", function () { it("will open a chatbox and be displayed inside it", mock.initConverse([], {}, async function (_converse) { const { api } = _converse; const include_nick = false; await mock.waitForRoster(_converse, 'current', 1, include_nick); await mock.openControlBox(_converse); const rosterview = document.querySelector('converse-roster'); await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length, 300); spyOn(_converse.api, "trigger").and.callThrough(); const message = 'This is a received message'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; // We don't already have an open chatbox for this user expect(_converse.chatboxes.get(sender_jid)).not.toBeDefined(); await _converse.handleMessageStanza( $msg({ 'from': sender_jid, 'to': api.connection.get().jid, 'type': 'chat', 'id': u.getUniqueId() }).c('body').t(message).up() .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree() ); const chatbox = await _converse.chatboxes.get(sender_jid); expect(chatbox).toBeDefined(); const view = _converse.chatboxviews.get(sender_jid); expect(view).toBeDefined(); expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); // Check that the message was received and check the message parameters await u.waitUntil(() => chatbox.messages.length); expect(chatbox.messages.length).toEqual(1); const msg_obj = chatbox.messages.models[0]; expect(msg_obj.get('message')).toEqual(message); expect(msg_obj.get('fullname')).toBeUndefined(); expect(msg_obj.get('sender')).toEqual('them'); expect(msg_obj.get('is_delayed')).toEqual(false); // Now check that the message appears inside the chatbox in the DOM const mel = await u.waitUntil(() => view.querySelector('.chat-msg .chat-msg__text')); expect(mel.textContent).toEqual(message); expect(view.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy(); await u.waitUntil(() => chatbox.vcard.get('fullname') === mock.cur_names[0]); await u.wa