converse.js
Version:
Browser based XMPP chat client
978 lines (826 loc) • 69.8 kB
JavaScript
/*global mock, converse, _ */
const $pres = converse.env.$pres;
const Strophe = converse.env.Strophe;
const sizzle = converse.env.sizzle;
const u = converse.env.utils;
describe("The Contacts Roster", function () {
beforeEach(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
it("verifies the origin of roster pushes", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
// See: https://gultsch.de/gajim_roster_push_and_message_interception.html
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.waitForRoster(_converse, 'current', 1);
expect(_converse.roster.models.length).toBe(1);
expect(_converse.roster.at(0).get('jid')).toBe(contact_jid);
spyOn(converse.env.log, 'warn');
let roster_push = stx`
<iq type="set" to="${_converse.jid}" from="eve@siacs.eu" xmlns="jabber:client">
<query xmlns='jabber:iq:roster'>
<item subscription="remove" jid="${contact_jid}"/>
</query>
</iq>`;
_converse.api.connection.get()._dataRecv(mock.createRequest(roster_push));
expect(converse.env.log.warn.calls.count()).toBe(1);
expect(converse.env.log.warn).toHaveBeenCalledWith(
`Ignoring roster illegitimate roster push message from eve@siacs.eu`
);
roster_push = stx`
<iq type="set" to="${_converse.jid}" from="eve@siacs.eu" xmlns="jabber:client">
<query xmlns='jabber:iq:roster'>
<item subscription="both" jid="eve@siacs.eu" name="${mock.cur_names[0]}" />
</query>
</iq>`;
_converse.api.connection.get()._dataRecv(mock.createRequest(roster_push));
expect(converse.env.log.warn.calls.count()).toBe(2);
expect(converse.env.log.warn).toHaveBeenCalledWith(
`Ignoring roster illegitimate roster push message from eve@siacs.eu`
);
expect(_converse.roster.models.length).toBe(1);
expect(_converse.roster.at(0).get('jid')).toBe(contact_jid);
}));
it("is populated once we have registered a presence handler", mock.initConverse([], {}, async function (_converse) {
const IQs = _converse.api.connection.get().IQ_stanzas;
const stanza = await u.waitUntil(
() => IQs.filter(iq => sizzle('iq query[xmlns="jabber:iq:roster"]', iq).length).pop());
expect(stanza).toEqualStanza(
stx`<iq id="${stanza.getAttribute('id')}" type="get" xmlns="jabber:client">
<query xmlns="jabber:iq:roster"/>
</iq>`);
const result = stx`
<iq to="${_converse.api.connection.get().jid}" type="result" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
<query xmlns="jabber:iq:roster">
<item jid="nurse@example.com"/>
<item jid="romeo@example.com"/>
</query>
</iq>`;
_converse.api.connection.get()._dataRecv(mock.createRequest(result));
await u.waitUntil(() => _converse.promises['rosterContactsFetched'].isResolved === true);
}));
it("supports roster versioning", mock.initConverse([], {}, async function (_converse) {
const { IQ_stanzas } = _converse.api.connection.get();
let stanza = await u.waitUntil(
() => IQ_stanzas.filter(iq => sizzle('iq query[xmlns="jabber:iq:roster"]', iq).length).pop());
const { roster } = _converse.state;
expect(roster.data.get('version')).toBeUndefined();
expect(stanza).toEqualStanza(stx`
<iq id="${stanza.getAttribute('id')}" type="get" xmlns="jabber:client">
<query xmlns="jabber:iq:roster"/>
</iq>`);
while (IQ_stanzas.length) IQ_stanzas.pop();
let result = stx`
<iq to="${_converse.api.connection.get().jid}" type="result" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
<query xmlns="jabber:iq:roster" ver="ver7">
<item jid="nurse@example.com"/>
<item jid="romeo@example.com"/>
</query>
</iq>`;
_converse.api.connection.get()._dataRecv(mock.createRequest(result));
await u.waitUntil(() => roster.models.length > 1);
expect(roster.data.get('version')).toBe('ver7');
expect(roster.models.length).toBe(2);
roster.fetchFromServer();
stanza = await u.waitUntil(
() => IQ_stanzas.filter(iq => sizzle('iq query[xmlns="jabber:iq:roster"]', iq).length).pop());
expect(stanza).toEqualStanza(
stx`<iq id="${stanza.getAttribute('id')}" type="get" xmlns="jabber:client">
<query ver="ver7" xmlns="jabber:iq:roster"/>
</iq>`);
result = stx`
<iq to="${_converse.api.connection.get().jid}" type="result" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
</iq>`;
_converse.api.connection.get()._dataRecv(mock.createRequest(result));
const roster_push = stx`
<iq type="set" to="${_converse.api.connection.get().jid}" xmlns="jabber:client">
<query xmlns='jabber:iq:roster' ver='ver34'>
<item jid='romeo@example.com' subscription='remove'/>
</query>
</iq>`;
_converse.api.connection.get()._dataRecv(mock.createRequest(roster_push));
expect(roster.data.get('version')).toBe('ver34');
expect(roster.models.length).toBe(1);
expect(roster.at(0).get('jid')).toBe('nurse@example.com');
}));
it("can ignore roster versioning", mock.initConverse([], { enable_roster_versioning: false }, async function (_converse) {
const { IQ_stanzas } = _converse.api.connection.get();
let stanza = await u.waitUntil(
() => IQ_stanzas.filter(iq => sizzle('iq query[xmlns="jabber:iq:roster"]', iq).length).pop());
const { roster } = _converse.state;
expect(roster.data.get('version')).toBeUndefined();
expect(stanza).toEqualStanza(stx`
<iq id="${stanza.getAttribute('id')}" type="get" xmlns="jabber:client">
<query xmlns="jabber:iq:roster"/>
</iq>`);
while (IQ_stanzas.length) IQ_stanzas.pop();
let result = stx`
<iq to="${_converse.api.connection.get().jid}" type="result" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
<query xmlns="jabber:iq:roster" ver="ver7">
<item jid="nurse@example.com"/>
<item jid="romeo@example.com"/>
</query>
</iq>`;
_converse.api.connection.get()._dataRecv(mock.createRequest(result));
await u.waitUntil(() => roster.models.length > 1);
expect(roster.data.get('version')).toBe('ver7');
expect(roster.models.length).toBe(2);
roster.fetchFromServer();
stanza = await u.waitUntil(
() => IQ_stanzas.filter(iq => sizzle('iq query[xmlns="jabber:iq:roster"]', iq).length).pop());
expect(stanza).toEqualStanza(
stx`<iq id="${stanza.getAttribute('id')}" type="get" xmlns="jabber:client">
<query xmlns="jabber:iq:roster"/>
</iq>`);
result = stx`
<iq to="${_converse.api.connection.get().jid}" type="result" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
<query xmlns="jabber:iq:roster" ver="ver8">
<item jid="nurse@example.com"/>
<item jid='romeo@example.com' subscription='remove'/>
</query>
</iq>`;
_converse.api.connection.get()._dataRecv(mock.createRequest(result));
await u.waitUntil(() => roster.data.get('version') === 'ver8');
expect(roster.models.length).toBe(1);
expect(roster.at(0).get('jid')).toBe('nurse@example.com');
}));
it("also contains contacts with subscription of none", mock.initConverse(
[], {}, async function (_converse) {
const { IQ_stanzas } = _converse.api.connection.get();
let stanza = await u.waitUntil(
() => IQ_stanzas.filter(iq => sizzle('iq query[xmlns="jabber:iq:roster"]', iq).length).pop());
_converse.api.connection.get()._dataRecv(mock.createRequest(stx`
<iq to="${_converse.api.connection.get().jid}" type="result" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
<query xmlns="jabber:iq:roster">
<item jid="juliet@example.net" name="Juliet" subscription="both">
<group>Friends</group>
</item>
<item jid="mercutio@example.net" name="Mercutio" subscription="from">
<group>Friends</group>
</item>
<item jid="lord.capulet@example.net" name="Lord Capulet" subscription="none">
<group>Acquaintences</group>
</item>
</query>
</iq>
`));
while (IQ_stanzas.length) IQ_stanzas.pop();
await u.waitUntil(() => _converse.roster.length === 3);
expect(_converse.roster.pluck('jid')).toEqual(['juliet@example.net', 'mercutio@example.net', 'lord.capulet@example.net']);
expect(_converse.roster.get('lord.capulet@example.net').get('subscription')).toBe('none');
}));
it("can be refreshed if loglevel is set to debug", mock.initConverse(
[], {loglevel: 'debug'}, async function (_converse) {
const { IQ_stanzas } = _converse.api.connection.get();
let stanza = await u.waitUntil(
() => IQ_stanzas.filter(iq => sizzle('iq query[xmlns="jabber:iq:roster"]', iq).length).pop());
_converse.api.connection.get()._dataRecv(mock.createRequest(stx`
<iq to="${_converse.api.connection.get().jid}" type="result" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
<query xmlns="jabber:iq:roster">
<item jid="juliet@example.net" name="Juliet" subscription="both">
<group>Friends</group>
</item>
<item jid="mercutio@example.net" name="Mercutio" subscription="from">
<group>Friends</group>
</item>
</query>
</iq>
`));
while (IQ_stanzas.length) IQ_stanzas.pop();
await u.waitUntil(() => _converse.roster.length === 2);
expect(_converse.roster.pluck('jid')).toEqual(['juliet@example.net', 'mercutio@example.net']);
await mock.openControlBox(_converse);
const rosterview = document.querySelector('converse-roster');
const dropdown = await u.waitUntil(
() => rosterview.querySelector('.dropdown--contacts')
);
const sync_button = dropdown.querySelector('.sync-contacts');
sync_button.click();
stanza = await u.waitUntil(
() => IQ_stanzas.filter(iq => sizzle('iq query[xmlns="jabber:iq:roster"]', iq).length).pop());
_converse.api.connection.get()._dataRecv(mock.createRequest(stx`
<iq to="${_converse.api.connection.get().jid}" type="result" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
<query xmlns="jabber:iq:roster">
<item jid="juliet@example.net" name="Juliet" subscription="both">
<group>Friends</group>
</item>
<item jid="lord.capulet@example.net" name="Lord Capulet" subscription="from">
<group>Acquaintences</group>
</item>
</query>
</iq>
`));
await u.waitUntil(() => _converse.roster.pluck('jid').includes('lord.capulet@example.net'));
expect(_converse.roster.pluck('jid')).toEqual(['juliet@example.net', 'lord.capulet@example.net']);
}));
it("will also show contacts added afterwards", mock.initConverse([], {}, async function (_converse) {
await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, 'current');
const rosterview = document.querySelector('converse-roster');
const roster = rosterview.querySelector('.roster-contacts');
const dropdown = await u.waitUntil(
() => rosterview.querySelector('.dropdown--contacts')
);
dropdown.querySelector('.toggle-filter').click();
const filter = await u.waitUntil(() => rosterview.querySelector('.items-filter'));
await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 18), 800);
filter.value = "la";
u.triggerEvent(filter, "keydown", "KeyboardEvent");
await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 4), 800);
// Five roster contact is now visible
const visible_contacts = sizzle('li', roster).filter(u.isVisible);
expect(visible_contacts.length).toBe(4);
let visible_groups = sizzle('.roster-group', roster).filter(u.isVisible).map(el => el.querySelector('a.group-toggle'));
expect(visible_groups.length).toBe(4);
expect(visible_groups[0].textContent.trim()).toBe('Colleagues');
expect(visible_groups[1].textContent.trim()).toBe('Family');
expect(visible_groups[2].textContent.trim()).toBe('friends & acquaintences');
expect(visible_groups[3].textContent.trim()).toBe('ænemies');
_converse.roster.create({
jid: 'lad@montague.lit',
subscription: 'both',
ask: null,
groups: ['newgroup'],
fullname: 'Lad'
});
await u.waitUntil(() => sizzle('.roster-group[data-group="newgroup"] li', roster).length, 300);
visible_groups = sizzle('.roster-group', roster).filter(u.isVisible).map(el => el.querySelector('a.group-toggle'));
expect(visible_groups.length).toBe(5);
expect(visible_groups[0].textContent.trim()).toBe('Colleagues');
expect(visible_groups[1].textContent.trim()).toBe('Family');
expect(visible_groups[2].textContent.trim()).toBe('friends & acquaintences');
expect(visible_groups[3].textContent.trim()).toBe('newgroup');
expect(visible_groups[4].textContent.trim()).toBe('ænemies');
expect(roster.querySelectorAll('.roster-group').length).toBe(5);
}));
describe("The live filter", function () {
it("will only be an option when there are more than 5 contacts",
mock.initConverse([], { show_self_in_roster: false }, async function (_converse) {
expect(document.querySelector('converse-roster')).toBe(null);
await mock.waitForRoster(_converse, 'current', 5);
await mock.openControlBox(_converse);
const view = _converse.chatboxviews.get('controlbox');
const dropdown = await u.waitUntil(
() => view.querySelector('.dropdown--contacts')
);
expect(dropdown.querySelector('.toggle-filter')).toBe(null);
mock.createContact(_converse, 'Slowpoke', 'subscribe');
const el = await u.waitUntil(() => dropdown.querySelector('.toggle-filter'));
expect(el).toBeDefined();
el.click();
await u.waitUntil(() => view.querySelector('.roster-contacts converse-list-filter'));
}));
it("can be used to filter the contacts shown",
mock.initConverse(
[], {'roster_groups': true},
async function (_converse) {
await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, 'current');
const rosterview = document.querySelector('converse-roster');
const roster = rosterview.querySelector('.roster-contacts');
await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 18), 600);
expect(sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length).toBe(5);
const filter_toggle = await u.waitUntil(() => rosterview.querySelector('.toggle-filter'));
filter_toggle.click();
let filter = await u.waitUntil(() => rosterview.querySelector('.items-filter'));
filter.value = "juliet";
u.triggerEvent(filter, "keydown", "KeyboardEvent");
await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 1), 600);
// Only one roster contact is now visible
let visible_contacts = sizzle('li', roster).filter(u.isVisible);
expect(visible_contacts.length).toBe(1);
expect(visible_contacts.pop().querySelector('.contact-name').textContent.trim()).toBe('Juliet Capulet');
// Only one foster group is still visible
expect(sizzle('.roster-group', roster).filter(u.isVisible).length).toBe(1);
const visible_group = sizzle('.roster-group', roster).filter(u.isVisible).pop();
expect(visible_group.querySelector('a.group-toggle').textContent.trim()).toBe('friends & acquaintences');
filter = rosterview.querySelector('.items-filter');
filter.value = "j";
u.triggerEvent(filter, "keydown", "KeyboardEvent");
await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 2), 700);
visible_contacts = sizzle('li', roster).filter(u.isVisible);
expect(visible_contacts.length).toBe(2);
let visible_groups = sizzle('.roster-group', roster).filter(u.isVisible).map(el => el.querySelector('a.group-toggle'));
expect(visible_groups.length).toBe(2);
expect(visible_groups[0].textContent.trim()).toBe('friends & acquaintences');
expect(visible_groups[1].textContent.trim()).toBe('Ungrouped');
filter = rosterview.querySelector('.items-filter');
filter.value = "xxx";
u.triggerEvent(filter, "keydown", "KeyboardEvent");
await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 0), 600);
visible_groups = sizzle('.roster-group', roster).filter(u.isVisible).map(el => el.querySelector('a.group-toggle'));
expect(visible_groups.length).toBe(0);
filter = rosterview.querySelector('.items-filter');
filter.value = "";
u.triggerEvent(filter, "keydown", "KeyboardEvent");
await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 18), 600);
expect(sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length).toBe(5);
}));
it("can be used to filter the groups shown", mock.initConverse([], {'roster_groups': true}, async function (_converse) {
await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, 'current');
const rosterview = document.querySelector('converse-roster');
const roster = rosterview.querySelector('.roster-contacts');
const filter_toggle = await u.waitUntil(() => rosterview.querySelector('.toggle-filter'));
filter_toggle.click();
const button = await u.waitUntil(() => rosterview.querySelector('converse-icon[data-type="groups"]'));
button.click();
await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 18), 600);
expect(sizzle('.roster-group', roster).filter(u.isVisible).length).toBe(5);
let filter = rosterview.querySelector('.items-filter');
filter.value = "colleagues";
u.triggerEvent(filter, "keydown", "KeyboardEvent");
await u.waitUntil(() => (sizzle('div.roster-group:not(.collapsed)', roster).length === 1), 600);
expect(sizzle('div.roster-group:not(.collapsed)', roster).pop().firstElementChild.textContent.trim()).toBe('Colleagues');
expect(sizzle('div.roster-group:not(.collapsed) li', roster).filter(u.isVisible).length).toBe(6);
// Check that all contacts under the group are shown
expect(sizzle('div.roster-group:not(.collapsed) li .open-chat', roster).filter(l => !u.isVisible(l)).length).toBe(0);
filter = rosterview.querySelector('.items-filter');
filter.value = "xxx";
u.triggerEvent(filter, "keydown", "KeyboardEvent");
await u.waitUntil(() => (roster.querySelectorAll('.roster-group').length === 0), 700);
filter = rosterview.querySelector('.items-filter');
filter.value = ""; // Check that groups are shown again, when the filter string is cleared.
u.triggerEvent(filter, "keydown", "KeyboardEvent");
await u.waitUntil(() => (roster.querySelectorAll('div.roster-group.collapsed').length === 0), 700);
expect(sizzle('div.roster-group', roster).length).toBe(0);
}));
it("has a button with which its contents can be cleared",
mock.initConverse([], {'roster_groups': true}, async function (_converse) {
await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, 'current');
const rosterview = document.querySelector('converse-roster');
const filter_toggle = await u.waitUntil(() => rosterview.querySelector('.toggle-filter'));
filter_toggle.click();
const filter = await u.waitUntil(() => rosterview.querySelector('.items-filter'));
filter.value = "xxx";
u.triggerEvent(filter, "keydown", "KeyboardEvent");
expect(filter.classList.contains("x")).toBeFalsy();
expect(u.hasClass('hidden', rosterview.querySelector('.items-filter-form .clear-input'))).toBeTruthy();
const isHidden = (el) => u.hasClass('hidden', el);
await u.waitUntil(() => !isHidden(rosterview.querySelector('.items-filter-form .clear-input')), 900);
rosterview.querySelector('.clear-input').click();
await u.waitUntil(() => document.querySelector('.items-filter').value == '');
}));
it("can be used to filter contacts by their chat state",
mock.initConverse(
[], {},
async function (_converse) {
await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, 'all');
let jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@montague.lit';
_converse.roster.get(jid).presence.set('presence', 'online');
jid = mock.cur_names[4].replace(/ /g,'.').toLowerCase() + '@montague.lit';
_converse.roster.get(jid).presence.set({ show: 'dnd', presence: 'online' });
await mock.openControlBox(_converse);
const rosterview = document.querySelector('converse-roster');
const filter_toggle = await u.waitUntil(() => rosterview.querySelector('.toggle-filter'));
filter_toggle.click();
const button = await u.waitUntil(() => rosterview.querySelector('converse-icon[data-type="state"]'));
button.click();
const filter = rosterview.querySelector('.state-type');
filter.value = "";
u.triggerEvent(filter, 'change');
const roster = rosterview.querySelector('.roster-contacts');
await u.waitUntil(() => sizzle('li', roster).filter(u.isVisible).length === 21, 900);
expect(sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length).toBe(6);
filter.value = "online";
u.triggerEvent(filter, 'change');
await u.waitUntil(() => sizzle('li', roster).filter(u.isVisible).length === 2, 900);
const contacts = sizzle('li', roster).filter(u.isVisible);
expect(contacts.pop().querySelector('.contact-name').textContent.trim()).toBe('Romeo Montague (me)');
expect(contacts.pop().querySelector('.contact-name').textContent.trim()).toBe('Lord Montague');
const groups = sizzle('ul.roster-group-contacts', roster).filter(u.isVisible);
expect(groups.pop().parentElement.firstElementChild.textContent.trim()).toBe('Ungrouped');
expect(groups.pop().parentElement.firstElementChild.textContent.trim()).toBe('Family');
filter.value = "dnd";
u.triggerEvent(filter, 'change');
await u.waitUntil(() => sizzle('li', roster).filter(u.isVisible).pop().querySelector('.contact-name').textContent.trim() === 'Friar Laurence', 900);
const ul = sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).pop();
expect(ul.parentElement.firstElementChild.textContent.trim()).toBe('friends & acquaintences');
expect(sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length).toBe(1);
}));
});
describe("A Roster Group", function () {
it("is created to show contacts with unread messages",
mock.initConverse(
[], { roster_groups: true },
async function (_converse) {
await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, 'all');
await mock.createContacts(_converse, 'requesting');
// Check that the groups appear alphabetically and that
// requesting and pending contacts are last.
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => sizzle('.roster-group a.group-toggle', rosterview).length === 7);
let group_titles = sizzle('.roster-group a.group-toggle', rosterview).map(o => o.textContent.trim());
expect(group_titles).toEqual([
"Contact requests",
"Colleagues",
"Family",
"friends & acquaintences",
"ænemies",
"Ungrouped",
"Pending contacts",
]);
const contact_jid = mock.getContactJID(0);
const contact = await _converse.api.contacts.get(contact_jid);
contact.save({'num_unread': 5});
await u.waitUntil(() => sizzle('.roster-group a.group-toggle', rosterview).length === 8);
group_titles = sizzle('.roster-group a.group-toggle', rosterview).map(o => o.textContent.trim());
expect(group_titles).toEqual([
"New messages",
"Contact requests",
"Colleagues",
"Family",
"friends & acquaintences",
"ænemies",
"Ungrouped",
"Pending contacts",
]);
const contacts = sizzle('.roster-group[data-group="New messages"] li converse-roster-contact', rosterview);
expect(contacts.length).toBe(1);
expect(contacts[0].querySelector('.contact-name').textContent).toBe("Mercutio");
expect(contacts[0].querySelector('.msgs-indicator').textContent).toBe("5");
contact.save({'num_unread': 0});
await u.waitUntil(() => sizzle('.roster-group a.group-toggle', rosterview).length === 7);
group_titles = sizzle('.roster-group a.group-toggle', rosterview).map(o => o.textContent.trim());
expect(group_titles).toEqual([
"Contact requests",
"Colleagues",
"Family",
"friends & acquaintences",
"ænemies",
"Ungrouped",
"Pending contacts",
]);
}));
it("can be used to organize existing contacts",
mock.initConverse(
[], { roster_groups: true, show_self_in_roster: false },
async function (_converse) {
await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, 'all');
await mock.createContacts(_converse, 'requesting');
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => sizzle('.roster-group a.group-toggle', rosterview).length === 7);
const group_titles = sizzle('.roster-group a.group-toggle', rosterview).map(o => o.textContent.trim());
expect(group_titles).toEqual([
"Contact requests",
"Colleagues",
"Family",
"friends & acquaintences",
"ænemies",
"Ungrouped",
"Pending contacts",
]);
// Check that usernames appear alphabetically per group
Object.keys(mock.groups).forEach(name => {
const contacts = sizzle('.roster-group[data-group="'+name+'"] ul .open-chat .contact-name', rosterview);
const names = contacts.map(o => o.textContent.trim());
const sorted_names = [...names];
sorted_names.sort();
expect(names).toEqual(sorted_names);
});
}));
it("gets created when a contact's \"groups\" attribute changes",
mock.initConverse([], {roster_groups: true, show_self_in_roster: false}, async function (_converse) {
await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, 'current', 0);
_converse.roster.create({
jid: 'groupchanger@montague.lit',
subscription: 'both',
ask: null,
groups: ['firstgroup'],
fullname: 'George Groupchanger'
});
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => sizzle('.roster-group a.group-toggle', rosterview).length === 1);
let group_titles = await u.waitUntil(() => {
const toggles = sizzle('.roster-group a.group-toggle', rosterview);
if (toggles.reduce((result, t) => result && u.isVisible(t), true)) {
return toggles.map(o => o.textContent.trim());
} else {
return false;
}
}, 1000);
expect(group_titles).toEqual(['firstgroup']);
const contact = _converse.roster.get('groupchanger@montague.lit');
contact.set({'groups': ['secondgroup']});
await u.waitUntil(() => sizzle('.roster-group[data-group="secondgroup"] a.group-toggle', rosterview).length);
group_titles = await u.waitUntil(() => {
const toggles = sizzle('.roster-group[data-group="secondgroup"] a.group-toggle', rosterview);
if (toggles.reduce((result, t) => result && u.isVisible(t), true)) {
return toggles.map(o => o.textContent.trim());
} else {
return false;
}
}, 1000);
expect(group_titles).toEqual(['secondgroup']);
}));
it("can share contacts with other roster groups",
mock.initConverse( [], {'roster_groups': true}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0);
const groups = ['Colleagues', 'friends'];
await mock.openControlBox(_converse);
for (let i=0; i<mock.cur_names.length; i++) {
_converse.roster.create({
jid: mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit',
subscription: 'both',
ask: null,
groups: groups,
fullname: mock.cur_names[i]
});
}
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => (sizzle('li', rosterview).filter(u.isVisible).length === 31));
// Check that usernames appear alphabetically per group
groups.forEach(name => {
const contacts = sizzle('.roster-group[data-group="'+name+'"] ul li .open-chat', rosterview);
const names = contacts.map(o => o.textContent.trim());
const sorted_names = [...names];
sorted_names.sort();
expect(names).toEqual(sorted_names);
expect(names.length).toEqual(mock.cur_names.length);
});
}));
it("remembers whether it is closed or opened",
mock.initConverse([], { show_self_in_roster: false }, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0);
await mock.openControlBox(_converse);
let i=0, j=0;
const groups = {
'Colleagues': 3,
'friends & acquaintences': 3,
'Ungrouped': 2
};
Object.keys(groups).forEach(function (name) {
j = i;
for (i=j; i<j+groups[name]; i++) {
_converse.roster.create({
jid: mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit',
subscription: 'both',
ask: null,
groups: name === 'ungrouped'? [] : [name],
fullname: mock.cur_names[i]
});
}
});
const state = _converse.roster.state;
expect(state.get('collapsed_groups')).toEqual([]);
const rosterview = document.querySelector('converse-roster');
const toggle = await u.waitUntil(() => rosterview.querySelector('a.group-toggle'));
toggle.click();
await u.waitUntil(() => state.get('collapsed_groups').length);
expect(state.get('collapsed_groups')).toEqual(['Colleagues']);
toggle.click();
expect(state.get('collapsed_groups')).toEqual([]);
}));
});
describe("Pending Contacts", function () {
it("can be collapsed under their own header (if roster_groups is false)",
mock.initConverse([], { roster_groups: false }, async function (_converse) {
await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, 'all');
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => sizzle('.roster-group', rosterview).filter(u.isVisible).map(e => e.querySelector('li')).length, 1000);
await mock.checkHeaderToggling.apply(_converse, [rosterview.querySelector('[data-group="Pending contacts"]')]);
}));
it("can be added to the roster",
mock.initConverse(
[], {},
async function (_converse) {
await mock.waitForRoster(_converse, 'all', 0);
await mock.openControlBox(_converse);
const rosterview = document.querySelector('converse-roster');
_converse.roster.create({
jid: mock.pend_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
subscription: 'none',
ask: 'subscribe',
fullname: mock.pend_names[0]
});
expect(u.isVisible(rosterview)).toBe(true);
await u.waitUntil(() => sizzle('li', rosterview).filter(u.isVisible).length === 1);
}));
it("are shown in the roster when hide_offline_users",
mock.initConverse(
[], { hide_offline_users: true, lazy_load_vcards: false },
async function (_converse) {
await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, 'pending');
await Promise.all(_converse.roster.map(contact => u.waitUntil(() => contact.vcard.get('fullname'))));
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => sizzle('li', rosterview).filter(u.isVisible).length, 500)
expect(u.isVisible(rosterview)).toBe(true);
expect(sizzle('li', rosterview).filter(u.isVisible).length).toBe(4);
expect(sizzle('ul.roster-group-contacts', rosterview).filter(u.isVisible).length).toBe(2);
expect(sizzle('ul.roster-group-contacts', rosterview)
.filter(u.isVisible)
.map((el) => el.getAttribute('data-group'))).toEqual(['Ungrouped', 'Pending contacts']);
}));
it("can be removed by the user", mock.initConverse([], {
roster_groups: false,
lazy_load_vcards: false,
}, async function (_converse) {
const { api } = _converse;
await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, 'all');
await Promise.all(_converse.roster.map(contact => u.waitUntil(() => contact.vcard.get('fullname'))));
const name = mock.pend_names[0];
const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
const contact = _converse.roster.get(jid);
spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => sizzle(`.pending-xmpp-contact .contact-name:contains("${name}")`, rosterview).length, 500);
sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, rosterview).pop().click();
await u.waitUntil(() => !sizzle(`.pending-xmpp-contact .contact-name:contains("${name}")`, rosterview).length, 500);
expect(api.confirm).toHaveBeenCalled();
const { sent_stanzas } = api.connection.get();
let stanza = await u.waitUntil(() => sent_stanzas.find(iq => iq.querySelector('iq item[subscription="remove"]')));
expect(stanza).toEqualStanza(stx`
<iq type="set" xmlns="jabber:client" id="${stanza.getAttribute('id')}">
<query xmlns="jabber:iq:roster">
<item jid="lord.capulet@montague.lit" subscription="remove"/>
</query>
</iq>`);
stanza = await u.waitUntil(() => sent_stanzas.filter(s => s.matches('presence[type="unsubscribe"]')).pop());
expect(stanza).toEqualStanza(
stx`<presence to="${contact.get('jid')}" type="unsubscribe" xmlns="jabber:client"/>`);
}));
it("do not have a header if there aren't any",
mock.initConverse(
['VCardsInitialized'], {'roster_groups': false},
async function (_converse) {
await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, 'current', 0);
const name = mock.pend_names[0];
_converse.roster.create({
jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit',
subscription: 'none',
ask: 'subscribe',
fullname: name
});
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => {
const el = rosterview.querySelector(`ul[data-group="Pending contacts"]`);
return u.isVisible(el) && Array.from(el.querySelectorAll('li')).filter(li => u.isVisible(li)).length;
}, 700)
const remove_el = await u.waitUntil(() => sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, rosterview).pop());
spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
remove_el.click();
expect(_converse.api.confirm).toHaveBeenCalled();
const iq_stanzas = _converse.api.connection.get().IQ_stanzas;
await u.waitUntil(() => Strophe.serialize(iq_stanzas.at(-1)) ===
`<iq id="${iq_stanzas.at(-1).getAttribute('id')}" type="set" xmlns="jabber:client">`+
`<query xmlns="jabber:iq:roster">`+
`<item jid="lord.capulet@montague.lit" subscription="remove"/>`+
`</query>`+
`</iq>`);
const iq = iq_stanzas.at(-1);
const stanza = stx`<iq id="${iq.getAttribute('id')}" to="romeo@montague.lit/orchard" type="result" xmlns="jabber:client"/>`;
_converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => rosterview.querySelector(`ul[data-group="Pending contacts"]`) === null);
}));
it("can be removed by the user",
mock.initConverse(
[],
{ roster_groups: false, lazy_load_vcards: false },
async function (_converse) {
spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, 'pending');
await Promise.all(_converse.roster.map(contact => u.waitUntil(() => contact.vcard.get('fullname'))));
await u.waitUntil(() => _converse.roster.at(0).vcard.get('fullname'))
const rosterview = document.querySelector('converse-roster');
const sent_IQs = _converse.api.connection.get().IQ_stanzas;
for (let i=0; i<mock.pend_names.length; i++) {
const name = mock.pend_names[i];
const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
const el = rosterview.querySelector(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`);
el.click();
const stanza = await u.waitUntil(() => sent_IQs.find(iq => iq.querySelector('iq item[subscription="remove"]')));
expect(stanza).toEqualStanza(
stx`<iq type="set" xmlns="jabber:client" id="${stanza.getAttribute('id')}">
<query xmlns="jabber:iq:roster"><item jid="${jid}" subscription="remove"/></query>
</iq>`);
_converse.api.connection.get()._dataRecv(mock.createRequest(
stx`<iq id="${stanza.getAttribute('id')}" type="result" xmlns="jabber:client"></iq>`));
while (sent_IQs.length) sent_IQs.pop();
}
await u.waitUntil(() => rosterview.querySelector(`ul[data-group="Pending contacts"]`) === null);
}));
it("can be added to the roster and they will be sorted alphabetically",
mock.initConverse(
[], { roster_groups: false, lazy_load_vcards: false },
async function (_converse) {
await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, 'current');
await Promise.all(_converse.roster.map(contact => u.waitUntil(() => contact.vcard.get('fullname'))));
let i;
for (i=0; i<mock.pend_names.length; i++) {
_converse.roster.create({
jid: mock.pend_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit',
subscription: 'none',
ask: 'subscribe',
fullname: mock.pend_names[i]
});
}
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => sizzle('li', rosterview.querySelector(`ul[data-group="Pending contacts"]`)).filter(u.isVisible).length);
// Check that they are sorted alphabetically
const el = await u.waitUntil(() => rosterview.querySelector(`ul[data-group="Pending contacts"]`));
const spans = el.querySelectorAll('.pending-xmpp-contact .contact-name');
await u.waitUntil(
() => Array.from(spans).reduce((result, value) => result + value.textContent?.trim(), '') ===
mock.pend_names.slice(0,i+1).sort().join('')
);
expect(true).toBe(true);
}));
});
describe("Existing Contacts", function () {
async function _addContacts (_converse) {
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
await Promise.all(_converse.roster.map(contact => u.waitUntil(() => contact.vcard.get('fullname'))));
}
it("can be collapsed under their own header",
mock.initConverse(
[], { lazy_load_vcards: false },
async function (_converse) {
await _addContacts(_converse);
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => sizzle('li', rosterview).filter(u.isVisible).length, 500);
await mock.checkHeaderToggling.apply(_converse, [rosterview.querySelector('.roster-group')]);
}));
it("will be hidden when appearing under a collapsed group",
mock.initConverse(
[], { roster_groups: false, show_self_in_roster: false, lazy_load_vcards: false },
async function (_converse) {
await _addContacts(_converse);
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => sizzle('li', rosterview).filter(u.isVisible).length, 500);
rosterview.querySelector('.group-toggle').click();
const name = "Romeo Montague";
const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
_converse.roster.create({
ask: null,
fullname: name,
jid: jid,
requesting: false,
subscription: 'both'
});
await u.waitUntil(() => u.hasClass('collapsed', rosterview.querySelector(`ul[data-group="Colleagues"]`)) === true);
expect(true).toBe(true);
}));
it("will have their online statuses shown correctly",
mock.initConverse(
[], { lazy_load_vcards: false },
async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
await mock.openControlBox(_converse);
const icon_el = await u.waitUntil(() => document.querySelector('converse-roster-contact converse-icon'));
expect(icon_el.getAttribute('color')).toBe('var(--chat-status-offline)');
let pres = stx`<presence from="mercutio@montague.lit/resource" xmlns="jabber:client"/>`;
_converse.api.connection.get()._dataRecv(mock.createRequest(pres));
await u.waitUntil(() => icon_el.getAttribute('color') === 'var(--chat-status-online)');
pres = stx`<presence from="mercutio@montague.lit/resource" xmlns="jabber:client"><show>away</show></presence>`;
_converse.api.connection.get()._dataRecv(mock.createRequest(pres));
await u.waitUntil(() => icon_el.getAttribute('color') === 'var(--chat-status-away)');
pres = stx`<presence from="mercutio@montague.lit/resource" xmlns="jabber:client"><show>xa</show></presence>`;
_converse.api.connection.get()._dataRecv(mock.createRequest(pres));
await u.waitUntil(() => icon_el.getAttribute('color') === 'var(--chat-status-offline)');
pres = stx`<presence from="mercutio@montague.lit/resource" xmlns="jabber:client"><show>dnd</show></presence>`;
_converse.api.connection.get()._dataRecv(mock.createRequest(pres));
await u.waitUntil(() => icon_el.getAttribute('color') === 'var(--chat-status-busy)');
pres = stx`<presence from="mercutio@montague.lit/resource" type="unavailable" xmlns="jabber:client"/>`;
_converse.api.connection.get()._dataRecv(mock.createRequest(pres));
await u.waitUntil(() => icon_el.getAttribute('color') === 'var(--chat-status-offline)');
}));
it("can be added to the roster and they will be sorted alphabetically",
mock.initConverse(
[], { lazy_load_vcards: false },
async function (_converse) {
const { api } = _converse;
await mock.waitForRoster(_converse, 'current', 0);
const { roster } = _converse.state;
await mock.openControlBox(_converse);
const rosterview = document.querySelector('converse-roster');
await Promise.all(mock.cur_names.map(name => {
const contact = roster.create({
jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit',
subscription: 'both',
ask: null,
fullname: name
});
return u.waitUntil(() => contact.initialized);
}));
await u.waitUntil(() => sizzle('li', rosterview).length);
await api.waitUntil('VCardsInitialized');
// Check that they are sorted alphabetically
const els = sizzle('.current-xmpp-contact.offline a.open-chat .contact-name', rosterview)
const t = els.reduce((result, value) => (result + value.textContent.trim()), '');
expect(t).toEqual(mock.cur_names.slice(0,mock.cur_names.length).sort().join(''));
// Check that ordering changes based on chat status
let contact_jid = mock.getContactJID(roster.length-1);
let contact = await _converse.api.contacts.get(contact_jid);
contact.presence.set('presence', 'online');
let sel = 'ul.roster-group-contacts li:first-child converse-roster-contact';
await u.waitUntil(() => rosterview.querySelector(sel).model?.get('jid') === contact_jid);
contact_jid = mock.getContactJID(roster.length-2);
contact = await _converse.api.contacts.get(conta