converse.js
Version:
Browser based XMPP chat client
937 lines (816 loc) • 74.2 kB
JavaScript
/*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&'+
'mlon=-122.399677#map=18/37.786971/-122.399677">https://www.openstreetmap.org/?mlat=37.786971&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('<p>This message contains <em>some</em> <b>markup</b></p>');
}));
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&utm_content=1&s=1">https://www.opkode.com/?id=0&utm_content=1&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