(function (root, factory) {
define(["jasmine", "mock", "test-utils" ], factory);
} (this, function (jasmine, mock, test_utils) {
const _ = converse.env._,
$pres = converse.env.$pres,
$iq = converse.env.$iq,
$msg = converse.env.$msg,
Strophe = converse.env.Strophe,
Promise = converse.env.Promise,
dayjs = converse.env.dayjs,
sizzle = converse.env.sizzle,
Backbone = converse.env.Backbone,
u = converse.env.utils;
describe("Groupchats", function () {
describe("The \"rooms\" API", function () {
it("has a method 'close' which closes rooms by JID or all rooms when called with no arguments",
mock.initConverse(
null, ['rosterGroupsFetched'], {},
async function (done, _converse) {
await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
await test_utils.openAndEnterChatRoom(_converse, 'leisure@montague.lit', 'romeo');
await test_utils.openAndEnterChatRoom(_converse, 'news@montague.lit', 'romeo');
expect(u.isVisible(_converse.chatboxviews.get('lounge@montague.lit').el)).toBeTruthy();
expect(u.isVisible(_converse.chatboxviews.get('leisure@montague.lit').el)).toBeTruthy();
expect(u.isVisible(_converse.chatboxviews.get('news@montague.lit').el)).toBeTruthy();
_converse.api.roomviews.close('lounge@montague.lit');
expect(_converse.chatboxviews.get('lounge@montague.lit')).toBeUndefined();
expect(u.isVisible(_converse.chatboxviews.get('leisure@montague.lit').el)).toBeTruthy();
expect(u.isVisible(_converse.chatboxviews.get('news@montague.lit').el)).toBeTruthy();
_converse.api.roomviews.close(['leisure@montague.lit', 'news@montague.lit']);
expect(_converse.chatboxviews.get('lounge@montague.lit')).toBeUndefined();
expect(_converse.chatboxviews.get('leisure@montague.lit')).toBeUndefined();
expect(_converse.chatboxviews.get('news@montague.lit')).toBeUndefined();
await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
await test_utils.openAndEnterChatRoom(_converse, 'leisure@montague.lit', 'romeo');
expect(u.isVisible(_converse.chatboxviews.get('lounge@montague.lit').el)).toBeTruthy();
expect(u.isVisible(_converse.chatboxviews.get('leisure@montague.lit').el)).toBeTruthy();
_converse.api.roomviews.close();
expect(_converse.chatboxviews.get('lounge@montague.lit')).toBeUndefined();
expect(_converse.chatboxviews.get('leisure@montague.lit')).toBeUndefined();
done();
}));
it("has a method 'get' which returns a wrapped groupchat (if it exists)",
mock.initConverse(
null, ['rosterGroupsFetched'], {},
async function (done, _converse) {
test_utils.createContacts(_converse, 'current');
await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group .group-toggle').length, 300);
let muc_jid = 'chillout@montague.lit';
await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
let room = _converse.api.rooms.get(muc_jid);
expect(room instanceof Object).toBeTruthy();
let chatroomview = _converse.chatboxviews.get(muc_jid);
expect(chatroomview.is_chatroom).toBeTruthy();
expect(u.isVisible(chatroomview.el)).toBeTruthy();
chatroomview.close();
// Test with mixed case
muc_jid = 'Leisure@montague.lit';
await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
room = _converse.api.rooms.get(muc_jid);
expect(room instanceof Object).toBeTruthy();
chatroomview = _converse.chatboxviews.get(muc_jid.toLowerCase());
expect(u.isVisible(chatroomview.el)).toBeTruthy();
muc_jid = 'leisure@montague.lit';
room = _converse.api.rooms.get(muc_jid);
expect(room instanceof Object).toBeTruthy();
chatroomview = _converse.chatboxviews.get(muc_jid.toLowerCase());
expect(u.isVisible(chatroomview.el)).toBeTruthy();
muc_jid = 'leiSure@montague.lit';
room = _converse.api.rooms.get(muc_jid);
expect(room instanceof Object).toBeTruthy();
chatroomview = _converse.chatboxviews.get(muc_jid.toLowerCase());
expect(u.isVisible(chatroomview.el)).toBeTruthy();
chatroomview.close();
// Non-existing room
muc_jid = 'chillout2@montague.lit';
room = _converse.api.rooms.get(muc_jid);
expect(typeof room === 'undefined').toBeTruthy();
done();
}));
it("has a method 'open' which opens (optionally configures) and returns a wrapped chat box",
mock.initConverse(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
// Mock 'getRoomFeatures', otherwise the room won't be
// displayed as it waits first for the features to be returned
// (when it's a new room being created).
spyOn(_converse.ChatRoom.prototype, 'getRoomFeatures').and.callFake(() => Promise.resolve());
let jid = 'lounge@montague.lit';
let chatroomview, IQ_id;
test_utils.openControlBox();
test_utils.createContacts(_converse, 'current');
await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group .group-toggle').length);
let room = await _converse.api.rooms.open(jid);
// Test on groupchat that's not yet open
expect(room instanceof Backbone.Model).toBeTruthy();
chatroomview = _converse.chatboxviews.get(jid);
expect(chatroomview.is_chatroom).toBeTruthy();
await u.waitUntil(() => u.isVisible(chatroomview.el));
// Test again, now that the room exists.
room = await _converse.api.rooms.open(jid);
expect(room instanceof Backbone.Model).toBeTruthy();
chatroomview = _converse.chatboxviews.get(jid);
expect(chatroomview.is_chatroom).toBeTruthy();
expect(u.isVisible(chatroomview.el)).toBeTruthy();
chatroomview.close();
// Test with mixed case in JID
jid = 'Leisure@montague.lit';
room = await _converse.api.rooms.open(jid);
expect(room instanceof Backbone.Model).toBeTruthy();
chatroomview = _converse.chatboxviews.get(jid.toLowerCase());
await u.waitUntil(() => u.isVisible(chatroomview.el));
jid = 'leisure@montague.lit';
room = await _converse.api.rooms.open(jid);
expect(room instanceof Backbone.Model).toBeTruthy();
chatroomview = _converse.chatboxviews.get(jid.toLowerCase());
await u.waitUntil(() => u.isVisible(chatroomview.el));
jid = 'leiSure@montague.lit';
room = await _converse.api.rooms.open(jid);
expect(room instanceof Backbone.Model).toBeTruthy();
chatroomview = _converse.chatboxviews.get(jid.toLowerCase());
await u.waitUntil(() => u.isVisible(chatroomview.el));
chatroomview.close();
_converse.muc_instant_rooms = false;
const sendIQ = _converse.connection.sendIQ;
spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
IQ_id = sendIQ.bind(this)(iq, callback, errback);
});
// Test with configuration
room = await _converse.api.rooms.open('room@conference.example.org', {
'nick': 'some1',
'auto_configure': true,
'roomconfig': {
'getmemberlist': ['moderator', 'participant'],
'changesubject': false,
'membersonly': true,
'persistentroom': true,
'publicroom': true,
'roomdesc': 'Welcome to this groupchat',
'whois': 'anyone'
}
});
expect(room instanceof Backbone.Model).toBeTruthy();
chatroomview = _converse.chatboxviews.get('room@conference.example.org');
// We pretend this is a new room, so no disco info is returned.
const features_stanza = $iq({
from: 'room@conference.example.org',
'id': IQ_id,
'to': 'romeo@montague.lit/desktop',
'type': 'error'
}).c('error', {'type': 'cancel'})
.c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"});
_converse.connection._dataRecv(test_utils.createRequest(features_stanza));
/*
*
*
*
*
*
*
*/
const presence = $pres({
from:'room@conference.example.org/some1',
to:'romeo@montague.lit/pda'
})
.c('x', {xmlns:'http://jabber.org/protocol/muc#user'})
.c('item', {
affiliation: 'owner',
jid: 'romeo@montague.lit/pda',
role: 'moderator'
}).up()
.c('status', {code:'110'}).up()
.c('status', {code:'201'});
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect(_converse.connection.sendIQ).toHaveBeenCalled();
const IQ_stanzas = _converse.connection.IQ_stanzas;
const iq = IQ_stanzas.filter(s => s.querySelector(`query[xmlns="${Strophe.NS.MUC_OWNER}"]`)).pop();
expect(Strophe.serialize(iq)).toBe(
``+
``);
const node = u.toStanza(`
Configuration for room@conference.example.org
Complete and submit this form to configure the room.
http://jabber.org/protocol/muc#roomconfig
Room
1
moderator
participant
visitor
20
`);
spyOn(chatroomview.model, 'sendConfiguration').and.callThrough();
_converse.connection._dataRecv(test_utils.createRequest(node));
await u.waitUntil(() => chatroomview.model.sendConfiguration.calls.count() === 1);
const sent_stanza = IQ_stanzas.filter(s => s.getAttribute('type') === 'set').pop();
expect(sizzle('field[var="muc#roomconfig_roomname"] value', sent_stanza).pop().textContent.trim()).toBe('Room');
expect(sizzle('field[var="muc#roomconfig_roomdesc"] value', sent_stanza).pop().textContent.trim()).toBe('Welcome to this groupchat');
expect(sizzle('field[var="muc#roomconfig_persistentroom"] value', sent_stanza).pop().textContent.trim()).toBe('1');
expect(sizzle('field[var="muc#roomconfig_getmemberlist"] value', sent_stanza).map(e => e.textContent.trim()).join(' ')).toBe('moderator participant');
expect(sizzle('field[var="muc#roomconfig_publicroom"] value ', sent_stanza).pop().textContent.trim()).toBe('1');
expect(sizzle('field[var="muc#roomconfig_changesubject"] value', sent_stanza).pop().textContent.trim()).toBe('0');
expect(sizzle('field[var="muc#roomconfig_whois"] value ', sent_stanza).pop().textContent.trim()).toBe('anyone');
expect(sizzle('field[var="muc#roomconfig_membersonly"] value', sent_stanza).pop().textContent.trim()).toBe('1');
expect(sizzle('field[var="muc#roomconfig_historylength"] value', sent_stanza).pop().textContent.trim()).toBe('20');
done();
}));
});
describe("An instant groupchat", function () {
it("will be created when muc_instant_rooms is set to true",
mock.initConverse(
null, ['rosterGroupsFetched', 'chatBoxesFetched', 'emojisInitialized'], {},
async function (done, _converse) {
const IQ_stanzas = _converse.connection.IQ_stanzas;
const sendIQ = _converse.connection.sendIQ;
const muc_jid = 'lounge@montague.lit';
let sent_IQ, IQ_id;
spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
if (iq.nodeTree.getAttribute('to') === 'lounge@montague.lit') {
sent_IQ = iq;
IQ_id = sendIQ.bind(this)(iq, callback, errback);
} else {
sendIQ.bind(this)(iq, callback, errback);
}
});
await test_utils.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
let stanza = await u.waitUntil(() => _.filter(
IQ_stanzas,
iq => iq.querySelector(
`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
)).pop());
// We pretend this is a new room, so no disco info is returned.
/*
*
*
*
*
*
*
*
*/
const features_stanza = $iq({
'from': 'lounge@montague.lit',
'id': stanza.getAttribute('id'),
'to': 'romeo@montague.lit/desktop',
'type': 'error'
}).c('error', {'type': 'cancel'})
.c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"});
_converse.connection._dataRecv(test_utils.createRequest(features_stanza));
const view = _converse.chatboxviews.get('lounge@montague.lit');
spyOn(view.model, 'join').and.callThrough();
/*
*
*
*/
stanza = await u.waitUntil(() => _.filter(
IQ_stanzas,
s => sizzle(`iq[to="${muc_jid}"] query[node="x-roomuser-item"]`, s).length
).pop()
);
expect(Strophe.serialize(stanza)).toBe(
``+
``);
/*
*
*
*
*
*/
const result_stanza = $iq({
'type': 'error',
'id': stanza.getAttribute('id'),
'from': view.model.get('jid'),
'to': _converse.connection.jid
}).c('error', {'type': 'cancel'})
.c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"});
_converse.connection._dataRecv(test_utils.createRequest(result_stanza));
const input = await u.waitUntil(() => view.el.querySelector('input[name="nick"]'));
expect(view.model.get('connection_status')).toBe(converse.ROOMSTATUS.NICKNAME_REQUIRED);
input.value = 'nicky';
view.el.querySelector('input[type=submit]').click();
expect(view.model.join).toHaveBeenCalled();
await u.waitUntil(() => view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING);
// The user has just entered the room (because join was called)
// and receives their own presence from the server.
// See example 24:
// https://xmpp.org/extensions/xep-0045.html#enter-pres
//
/*
*
*
*
*
*
*
*/
const presence = $pres({
to:'romeo@montague.lit/orchard',
from:'lounge@montague.lit/nicky',
id:'5025e055-036c-4bc5-a227-706e7e352053'
}).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:'201'}).nodeTree;
_converse.connection._dataRecv(test_utils.createRequest(presence));
await u.waitUntil(() => view.model.get('connection_status') === converse.ROOMSTATUS.ENTERED);
await test_utils.returnMemberLists(_converse, muc_jid);
// await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length === 2);
const info_texts = Array.from(view.el.querySelectorAll('.chat-content .chat-info')).map(e => e.textContent.trim());
expect(info_texts[0]).toBe('A new groupchat has been created');
expect(info_texts[1]).toBe('nicky has entered the groupchat');
// An instant room is created by saving the default configuratoin.
//
/*
*
*
*/
const iq = IQ_stanzas.filter(s => s.querySelector(`query[xmlns="${Strophe.NS.MUC_OWNER}"]`)).pop();
expect(Strophe.serialize(iq)).toBe(
``+
``+
``);
done();
}));
});
describe("A Groupchat", function () {
describe("upon being entered", function () {
it("will fetch the member list if muc_fetch_members is true",
mock.initConverse(
null, ['rosterGroupsFetched'], {'muc_fetch_members': true},
async function (done, _converse) {
const sent_IQs = _converse.connection.IQ_stanzas;
const muc_jid = 'lounge@montague.lit';
spyOn(_converse.ChatRoomOccupants.prototype, 'fetchMembers').and.callThrough();
await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
let view = _converse.chatboxviews.get(muc_jid);
expect(view.model.occupants.fetchMembers).toHaveBeenCalled();
// Check in reverse order that we requested all three lists
const owner_iq = sent_IQs.pop();
expect(Strophe.serialize(owner_iq)).toBe(
``+
` `+
``);
const admin_iq = sent_IQs.pop();
expect(Strophe.serialize(admin_iq)).toBe(
``+
` `+
``);
const member_iq = sent_IQs.pop();
expect(Strophe.serialize(member_iq)).toBe(
``+
` `+
``);
_converse.muc_fetch_members = false;
await test_utils.openAndEnterChatRoom(_converse, 'orchard@montague.lit', 'romeo');
view = _converse.chatboxviews.get('orchard@montague.lit');
expect(view.model.occupants.fetchMembers.calls.count()).toBe(1);
done();
}));
describe("when fetching the member lists", function () {
it("gracefully handles being forbidden from fetching the lists for certain affiliations",
mock.initConverse(
null, ['rosterGroupsFetched'], {'muc_fetch_members': true},
async function (done, _converse) {
const sent_IQs = _converse.connection.IQ_stanzas;
const muc_jid = 'lounge@montague.lit';
const features = [
'http://jabber.org/protocol/muc',
'jabber:iq:register',
'muc_hidden',
'muc_membersonly',
'muc_passwordprotected',
Strophe.NS.MAM,
Strophe.NS.SID
];
const nick = 'romeo';
const room = Strophe.getNodeFromJid(muc_jid);
const server = Strophe.getDomainFromJid(muc_jid);
await _converse.api.rooms.open(muc_jid);
await test_utils.getRoomFeatures(_converse, room, server, features);
await test_utils.waitForReservedNick(_converse, muc_jid, nick);
test_utils.receiveOwnMUCPresence(_converse, muc_jid, nick);
const view = _converse.chatboxviews.get(muc_jid);
await u.waitUntil(() => (view.model.get('connection_status') === converse.ROOMSTATUS.ENTERED));
// Check in reverse order that we requested all three lists
const owner_iq = sent_IQs.pop();
expect(Strophe.serialize(owner_iq)).toBe(
``+
` `+
``);
const admin_iq = sent_IQs.pop();
expect(Strophe.serialize(admin_iq)).toBe(
``+
` `+
``);
const member_iq = sent_IQs.pop();
expect(Strophe.serialize(member_iq)).toBe(
``+
` `+
``);
// It might be that the user is not allowed to fetch certain lists.
let err_stanza = u.toStanza(
`
`);
_converse.connection._dataRecv(test_utils.createRequest(err_stanza));
err_stanza = u.toStanza(
`
`);
_converse.connection._dataRecv(test_utils.createRequest(err_stanza));
// Now the service sends the member lists to the user
const member_list_stanza = $iq({
'from': muc_jid,
'id': member_iq.getAttribute('id'),
'to': 'romeo@montague.lit/orchard',
'type': 'result'
}).c('query', {'xmlns': Strophe.NS.MUC_ADMIN})
.c('item', {
'affiliation': 'member',
'jid': 'hag66@shakespeare.lit',
'nick': 'thirdwitch',
'role': 'participant'
});
_converse.connection._dataRecv(test_utils.createRequest(member_list_stanza));
await u.waitUntil(() => view.model.occupants.length > 1);
expect(view.model.occupants.length).toBe(2);
// The existing owner occupant should not have their
// affiliation removed due to the owner list
// not being returned (forbidden err).
expect(view.model.occupants.findWhere({'jid': _converse.bare_jid}).get('affiliation')).toBe('owner');
expect(view.model.occupants.findWhere({'jid': 'hag66@shakespeare.lit'}).get('affiliation')).toBe('member');
done();
}));
});
});
it("clears cached messages when it gets closed and clear_messages_on_reconnection is true",
mock.initConverse(
null, ['rosterGroupsFetched'], {'clear_messages_on_reconnection': true},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
await test_utils.openAndEnterChatRoom(_converse, muc_jid , 'romeo');
const view = _converse.chatboxviews.get(muc_jid);
const message = 'Hello world',
nick = mock.chatroom_names[0],
msg = $msg({
'from': 'lounge@montague.lit/'+nick,
'id': (new Date()).getTime(),
'to': 'romeo@montague.lit',
'type': 'groupchat'
}).c('body').t(message).tree();
await view.model.onMessage(msg);
await new Promise((resolve, reject) => view.once('messageInserted', resolve));
spyOn(view.model, 'clearMessages').and.callThrough();
view.model.close();
await u.waitUntil(() => view.model.clearMessages.calls.count());
expect(view.model.messages.length).toBe(0);
expect(view.content.innerHTML).toBe('');
done()
}));
it("is opened when an xmpp: URI is clicked inside another groupchat",
mock.initConverse(
null, ['rosterGroupsFetched', 'emojisInitialized'], {},
async function (done, _converse) {
test_utils.createContacts(_converse, 'current');
await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
const view = _converse.chatboxviews.get('lounge@montague.lit');
if (!view.el.querySelectorAll('.chat-area').length) {
view.renderChatArea();
}
expect(_converse.chatboxes.length).toEqual(2);
const message = 'Please go to xmpp:coven@chat.shakespeare.lit?join',
nick = mock.chatroom_names[0],
msg = $msg({
'from': 'lounge@montague.lit/'+nick,
'id': (new Date()).getTime(),
'to': 'romeo@montague.lit',
'type': 'groupchat'
}).c('body').t(message).tree();
await view.model.onMessage(msg);
await new Promise((resolve, reject) => view.once('messageInserted', resolve));
view.el.querySelector('.chat-msg__text a').click();
await u.waitUntil(() => _converse.chatboxes.length === 3)
expect(_.includes(_converse.chatboxes.pluck('id'), 'coven@chat.shakespeare.lit')).toBe(true);
done()
}));
it("shows a notification if it's not anonymous",
mock.initConverse(
null, ['rosterGroupsFetched', 'chatBoxesFetched', 'emojisInitialized'], {},
async function (done, _converse) {
const sent_IQs = _converse.connection.IQ_stanzas;
const muc_jid = 'coven@chat.shakespeare.lit';
const room = Strophe.getNodeFromJid(muc_jid);
const server = Strophe.getDomainFromJid(muc_jid);
const nick = 'romeo';
await _converse.api.rooms.open(muc_jid);
await test_utils.getRoomFeatures(_converse, room, server);
await test_utils.waitForReservedNick(_converse, muc_jid, nick);
const view = _converse.chatboxviews.get(muc_jid);
const chat_content = view.el.querySelector('.chat-content');
/*
*
*
*
*
*
*