2
0
Эх сурвалжийг харах

Properly store contact connection status info in presences collection

JC Brand 2 сар өмнө
parent
commit
55e58e07e2

+ 11 - 3
src/headless/plugins/chat/model.js

@@ -47,10 +47,8 @@ class ChatBox extends ModelWithVCard(ModelWithMessages(ModelWithContact(ColorAwa
         this.initialized = getOpenPromise();
 
         const jid = this.get('jid');
-        const { presences } = _converse.state;
-        this.presence = presences.get(jid) || presences.create({ jid });
+        this.setPresence(jid);
         await this.setModelContact(jid);
-        this.presence.on('change:show', (item) => this.onPresenceChanged(item));
 
         this.on('change:chat_state', () => sendChatState(this.get('jid'), this.get('chat_state')));
         this.on('change:hidden', () => this.get('hidden') && this.setChatState(INACTIVE));
@@ -66,6 +64,16 @@ class ChatBox extends ModelWithVCard(ModelWithMessages(ModelWithContact(ColorAwa
         this.initialized.resolve();
     }
 
+    /**
+     * @param {string} jid
+     */
+    async setPresence(jid) {
+        await api.waitUntil('presencesInitialized');
+        const { presences } = _converse.state;
+        this.presence = presences.get(jid) || presences.create({ jid });
+        this.presence.on('change:show', (item) => this.onPresenceChanged(item));
+    }
+
     /**
      * @param {MessageAttributes|StanzaParseError} attrs_or_error
      */

+ 7 - 10
src/headless/plugins/roster/contact.js

@@ -26,7 +26,7 @@ class RosterContact extends ModelWithVCard(ColorAwareModel(Model)) {
         this.lazy_load_vcard = true;
         super.initialize();
         this.initialized = getOpenPromise();
-        this.setPresence();
+        await this.setPresence();
         const { jid } = attrs;
         this.set({
             ...attrs,
@@ -43,9 +43,9 @@ class RosterContact extends ModelWithVCard(ColorAwareModel(Model)) {
          * @example _converse.api.listen.on('contactPresenceChanged', contact => { ... });
          */
         this.listenTo(this.presence, 'change:show', () => api.trigger('contactPresenceChanged', this));
-        this.listenTo(this.presence, 'change:show', () => this.trigger('presenceChanged'));
+        this.listenTo(this.presence, 'change:show', () => this.trigger('presence:change'));
         this.listenTo(this.presence, 'change:presence', () => api.trigger('contactPresenceChanged', this));
-        this.listenTo(this.presence, 'change:presence', () => this.trigger('presenceChanged'));
+        this.listenTo(this.presence, 'change:presence', () => this.trigger('presence:change'));
         /**
          * Synchronous event which provides a hook for further initializing a RosterContact
          * @event _converse#rosterContactInitialized
@@ -55,18 +55,15 @@ class RosterContact extends ModelWithVCard(ColorAwareModel(Model)) {
         this.initialized.resolve();
     }
 
-    setPresence () {
+    async setPresence () {
         const jid = this.get('jid');
+        await api.waitUntil('presencesInitialized');
         const { presences } = _converse.state;
-        this.presence = presences.findWhere(jid) || presences.create({ jid });
+        this.presence = presences.get(jid) || presences.create({ jid });
     }
 
     getStatus () {
-        const presence  = this.presence.get('presence');
-        if (presence === 'offline' || presence === 'unavailable') {
-            return 'offline';
-        }
-        return this.presence.get('show') || presence || 'offline';
+        return this.presence?.getStatus() || 'offline';
     }
 
     openChat () {

+ 24 - 0
src/headless/plugins/roster/parsers.js

@@ -0,0 +1,24 @@
+import converse from '../../shared/api/public.js';
+
+const { Strophe, sizzle, dayjs } = converse.env;
+
+/**
+ * @param {Element} stanza
+ * @returns {import('./types').Presence}
+ */
+export function parsePresence(stanza) {
+    const jid = stanza.getAttribute('from');
+    const type = /** @type {import('./types').PresenceTypes} */(stanza.getAttribute('type'));
+    const resource = Strophe.getResourceFromJid(jid);
+    const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, stanza).pop();
+    const priority = stanza.querySelector('priority')?.textContent;
+    const show = /** @type {import('./types').PresenceShowValues|undefined} */(stanza.querySelector('show')?.textContent);
+    const timestamp = delay ? dayjs(delay.getAttribute('stamp')).toISOString() : new Date().toISOString();
+    return {
+        resource,
+        show,
+        timestamp,
+        type,
+        priority: Number.isNaN(parseInt(priority, 10)) ? 0 : parseInt(priority, 10),
+    };
+}

+ 7 - 1
src/headless/plugins/roster/plugin.js

@@ -32,7 +32,13 @@ converse.plugins.add('converse-roster', {
             synchronize_availability: true
         });
 
-        api.promises.add(['cachedRoster', 'roster', 'rosterContactsFetched', 'rosterInitialized']);
+        api.promises.add([
+            'cachedRoster',
+            'roster',
+            'rosterContactsFetched',
+            'rosterInitialized',
+            'presencesInitialized',
+        ]);
 
         // API methods only available to plugins
         Object.assign(_converse.api, roster_api);

+ 21 - 21
src/headless/plugins/roster/presence.js

@@ -1,20 +1,17 @@
 import Resources from './resources.js';
 import { Model } from '@converse/skeletor';
-import converse from '../../shared/api/public.js';
 import { initStorage } from '../../utils/storage.js';
-
-const { Strophe, dayjs, sizzle } = converse.env;
+import {parsePresence} from './parsers.js';
 
 class Presence extends Model {
     get idAttribute() {
-        // eslint-disable-line class-methods-use-this
         return 'jid';
     }
 
     defaults() {
-        // eslint-disable-line class-methods-use-this
         return {
-            'show': 'offline',
+            presence: 'offline',
+            show: null,
         };
     }
 
@@ -30,16 +27,21 @@ class Presence extends Model {
 
     onResourcesChanged() {
         const hpr = this.getHighestPriorityResource();
-        const show = hpr?.attributes?.show || 'offline';
-        if (this.get('show') !== show) {
-            this.save({ show });
+        const { presence, show } = hpr?.attributes ?? {};
+        this.save({ presence, show });
+    }
+
+    getStatus() {
+        const presence = this.get('presence');
+        if (presence === 'offline') {
+            return 'offline';
         }
+        return this.get('show') || presence || 'offline';
     }
 
     /**
      * Return the resource with the highest priority.
      * If multiple resources have the same priority, take the latest one.
-     * @private
      */
     getHighestPriorityResource() {
         return this.resources.sortBy((r) => `${r.get('priority')}-${r.get('timestamp')}`).reverse()[0];
@@ -49,20 +51,18 @@ class Presence extends Model {
      * Adds a new resource and it's associated attributes as taken
      * from the passed in presence stanza.
      * Also updates the presence if the resource has higher priority (and is newer).
-     * @param { Element } presence: The presence stanza
+     * @param {Element} presence: The presence stanza
      */
     addResource(presence) {
-        const jid = presence.getAttribute('from');
-        const name = Strophe.getResourceFromJid(jid);
-        const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, presence).pop();
-        const priority = presence.querySelector('priority')?.textContent;
-        const resource = this.resources.get(name);
+        const attrs = parsePresence(presence);
         const settings = {
-            name,
-            'priority': Number.isNaN(parseInt(priority, 10)) ? 0 : parseInt(priority, 10),
-            'show': presence.querySelector('show')?.textContent ?? 'online',
-            'timestamp': delay ? dayjs(delay.getAttribute('stamp')).toISOString() : new Date().toISOString(),
+            name: attrs.resource,
+            presence: attrs.type === 'unavailable' ? 'offline' : 'online',
+            priority: attrs.priority,
+            show: attrs.show,
+            timestamp: attrs.timestamp,
         };
+        const resource = this.resources.get(settings.name);
         if (resource) {
             resource.save(settings);
         } else {
@@ -74,7 +74,7 @@ class Presence extends Model {
      * Remove the passed in resource from the resources map.
      * Also redetermines the presence given that there's one less
      * resource.
-     * @param { string } name: The resource name
+     * @param {string} name: The resource name
      */
     removeResource(name) {
         const resource = this.resources.get(name);

+ 0 - 1
src/headless/plugins/roster/presences.js

@@ -2,7 +2,6 @@ import { Collection } from "@converse/skeletor";
 import Presence from "./presence.js";
 
 class Presences extends Collection {
-
     constructor () {
         super();
         this.model = Presence;

+ 57 - 18
src/headless/plugins/roster/tests/presence.js

@@ -21,10 +21,11 @@ describe("A received presence stanza", function () {
                 <delay xmlns="urn:xmpp:delay" stamp="2017-02-15T20:26:05Z" from="${contact_jid}/priority-1-resource"/>
             </presence>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        expect(contact.presence.get('show')).toBe('online');
+        expect(contact.presence.getStatus()).toBe('online');
         expect(contact.presence.resources.length).toBe(1);
         expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1);
-        expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online');
+        expect(contact.presence.resources.get('priority-1-resource').get('show')).toBeUndefined();
+        expect(contact.presence.resources.get('priority-1-resource').get('presence')).toBe('online');
 
         stanza = stx`
             <presence xmlns="jabber:client"
@@ -38,13 +39,14 @@ describe("A received presence stanza", function () {
                 <delay xmlns="urn:xmpp:delay" stamp="2017-02-15T17:02:24Z" from="'+contact_jid+'/priority-0-resource"/>
             </presence>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        expect(contact.presence.get('show')).toBe('online');
-
+        expect(contact.presence.getStatus()).toBe('online');
         expect(contact.presence.resources.length).toBe(2);
         expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0);
         expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa');
+        expect(contact.presence.resources.get('priority-0-resource').get('presence')).toBe('online');
         expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1);
-        expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online');
+        expect(contact.presence.resources.get('priority-1-resource').get('show')).toBeUndefined();
+        expect(contact.presence.resources.get('priority-1-resource').get('presence')).toBe('online');
 
         stanza = stx`
             <presence xmlns="jabber:client"
@@ -54,14 +56,19 @@ describe("A received presence stanza", function () {
                 <show>dnd</show>
             </presence>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        expect(contact.presence.get('show')).toBe('dnd');
+        expect(contact.presence.getStatus()).toBe('dnd');
         expect(contact.presence.resources.length).toBe(3);
         expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0);
         expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa');
+        expect(contact.presence.resources.get('priority-0-resource').get('presence')).toBe('online');
+
         expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1);
-        expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online');
+        expect(contact.presence.resources.get('priority-1-resource').get('show')).toBeUndefined();
+        expect(contact.presence.resources.get('priority-1-resource').get('presence')).toBe('online');
+
         expect(contact.presence.resources.get('priority-2-resource').get('priority')).toBe(2);
         expect(contact.presence.resources.get('priority-2-resource').get('show')).toBe('dnd');
+        expect(contact.presence.resources.get('priority-2-resource').get('presence')).toBe('online');
 
         stanza = stx`
             <presence xmlns="jabber:client"
@@ -71,16 +78,23 @@ describe("A received presence stanza", function () {
                 <show>away</show>
             </presence>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('away');
+        expect(_converse.roster.get(contact_jid).presence.getStatus()).toBe('away');
         expect(contact.presence.resources.length).toBe(4);
         expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0);
         expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa');
+        expect(contact.presence.resources.get('priority-0-resource').get('presence')).toBe('online');
+
         expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1);
-        expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online');
+        expect(contact.presence.resources.get('priority-1-resource').get('show')).toBeUndefined();
+        expect(contact.presence.resources.get('priority-1-resource').get('presence')).toBe('online');
+
         expect(contact.presence.resources.get('priority-2-resource').get('priority')).toBe(2);
         expect(contact.presence.resources.get('priority-2-resource').get('show')).toBe('dnd');
+        expect(contact.presence.resources.get('priority-2-resource').get('presence')).toBe('online');
+
         expect(contact.presence.resources.get('priority-3-resource').get('priority')).toBe(3);
         expect(contact.presence.resources.get('priority-3-resource').get('show')).toBe('away');
+        expect(contact.presence.resources.get('priority-3-resource').get('presence')).toBe('online');
 
         stanza = stx`
             <presence xmlns="jabber:client"
@@ -91,18 +105,27 @@ describe("A received presence stanza", function () {
                 <delay xmlns="urn:xmpp:delay" stamp="2017-02-15T15:02:24Z" from="${contact_jid}/older-priority-1-resource"/>
             </presence>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('away');
+        expect(_converse.roster.get(contact_jid).presence.getStatus()).toBe('away');
         expect(contact.presence.resources.length).toBe(5);
         expect(contact.presence.resources.get('older-priority-1-resource').get('priority')).toBe(1);
         expect(contact.presence.resources.get('older-priority-1-resource').get('show')).toBe('dnd');
+        expect(contact.presence.resources.get('older-priority-1-resource').get('presence')).toBe('online');
+
         expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0);
         expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa');
+        expect(contact.presence.resources.get('priority-0-resource').get('presence')).toBe('online');
+
         expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1);
-        expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online');
+        expect(contact.presence.resources.get('priority-1-resource').get('show')).toBeUndefined();
+        expect(contact.presence.resources.get('priority-1-resource').get('presence')).toBe('online');
+
         expect(contact.presence.resources.get('priority-2-resource').get('priority')).toBe(2);
         expect(contact.presence.resources.get('priority-2-resource').get('show')).toBe('dnd');
+        expect(contact.presence.resources.get('priority-2-resource').get('presence')).toBe('online');
+
         expect(contact.presence.resources.get('priority-3-resource').get('priority')).toBe(3);
         expect(contact.presence.resources.get('priority-3-resource').get('show')).toBe('away');
+        expect(contact.presence.resources.get('priority-3-resource').get('presence')).toBe('online');
 
         stanza = stx`
             <presence xmlns="jabber:client"
@@ -111,16 +134,23 @@ describe("A received presence stanza", function () {
                       from="${contact_jid}/priority-3-resource">
             </presence>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('dnd');
+        expect(_converse.roster.get(contact_jid).presence.getStatus()).toBe('dnd');
         expect(contact.presence.resources.length).toBe(4);
         expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0);
         expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa');
+        expect(contact.presence.resources.get('priority-0-resource').get('presence')).toBe('online');
+
         expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1);
-        expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online');
+        expect(contact.presence.resources.get('priority-1-resource').get('show')).toBeUndefined();
+        expect(contact.presence.resources.get('priority-1-resource').get('presence')).toBe('online');
+
         expect(contact.presence.resources.get('priority-2-resource').get('priority')).toBe(2);
         expect(contact.presence.resources.get('priority-2-resource').get('show')).toBe('dnd');
+        expect(contact.presence.resources.get('priority-2-resource').get('presence')).toBe('online');
+
         expect(contact.presence.resources.get('older-priority-1-resource').get('priority')).toBe(1);
         expect(contact.presence.resources.get('older-priority-1-resource').get('show')).toBe('dnd');
+        expect(contact.presence.resources.get('older-priority-1-resource').get('presence')).toBe('online');
 
         stanza = stx`
             <presence xmlns="jabber:client"
@@ -129,14 +159,19 @@ describe("A received presence stanza", function () {
                       from="${contact_jid}/priority-2-resource">
             </presence>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('online');
+        expect(_converse.roster.get(contact_jid).presence.getStatus()).toBe('online');
         expect(contact.presence.resources.length).toBe(3);
         expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0);
         expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa');
+        expect(contact.presence.resources.get('priority-0-resource').get('presence')).toBe('online');
+
         expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1);
-        expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online');
+        expect(contact.presence.resources.get('priority-1-resource').get('show')).toBeUndefined();
+        expect(contact.presence.resources.get('priority-1-resource').get('presence')).toBe('online');
+
         expect(contact.presence.resources.get('older-priority-1-resource').get('priority')).toBe(1);
         expect(contact.presence.resources.get('older-priority-1-resource').get('show')).toBe('dnd');
+        expect(contact.presence.resources.get('older-priority-1-resource').get('presence')).toBe('online');
 
         stanza = stx`
             <presence xmlns="jabber:client"
@@ -145,12 +180,15 @@ describe("A received presence stanza", function () {
                       from="${contact_jid}/priority-1-resource">
             </presence>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('dnd');
+        expect(_converse.roster.get(contact_jid).presence.getStatus()).toBe('dnd');
         expect(contact.presence.resources.length).toBe(2);
         expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0);
         expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa');
+        expect(contact.presence.resources.get('priority-0-resource').get('presence')).toBe('online');
+
         expect(contact.presence.resources.get('older-priority-1-resource').get('priority')).toBe(1);
         expect(contact.presence.resources.get('older-priority-1-resource').get('show')).toBe('dnd');
+        expect(contact.presence.resources.get('older-priority-1-resource').get('presence')).toBe('online');
 
         stanza = stx`
             <presence xmlns="jabber:client"
@@ -159,10 +197,11 @@ describe("A received presence stanza", function () {
                       from="${contact_jid}/older-priority-1-resource">
             </presence>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('xa');
+        expect(_converse.roster.get(contact_jid).presence.getStatus()).toBe('xa');
         expect(contact.presence.resources.length).toBe(1);
         expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0);
         expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa');
+        expect(contact.presence.resources.get('priority-0-resource').get('presence')).toBe('online');
 
         stanza = stx`
             <presence xmlns="jabber:client"
@@ -171,7 +210,7 @@ describe("A received presence stanza", function () {
                       from="${contact_jid}/priority-0-resource">
             </presence>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('offline');
+        expect(_converse.roster.get(contact_jid).presence.getStatus()).toBe('offline');
         expect(contact.presence.resources.length).toBe(0);
     }));
 });

+ 24 - 4
src/headless/plugins/roster/types.ts

@@ -1,18 +1,38 @@
+export type PresenceTypes =
+    | null
+    | 'available'
+    | 'unavailable'
+    | 'error'
+    | 'probe'
+    | 'subscribe'
+    | 'subscribed'
+    | 'unsubscribe'
+    | 'unsubscribed';
+export type PresenceShowValues = 'chat' | 'away' | 'dnd' | 'xa';
+
+export type Presence = {
+    resource: string;
+    type: PresenceTypes;
+    priority: Number;
+    show?: PresenceShowValues;
+    timestamp: string;
+};
+
 export type RosterContactUpdateAttrs = {
     nickname?: string; // The name of that user
     groups?: string[]; // Any roster groups the user might belong to
-}
+};
 
 export type RosterContactAttributes = {
     jid: string;
-    subscription: ('none'|'to'|'from'|'both');
+    subscription: 'none' | 'to' | 'from' | 'both';
     ask?: 'subscribe'; // The Jabber ID of the user being added and subscribed to
     name?: string; // The name of that user
     groups?: string[]; // Any roster groups the user might belong to
     requesting?: boolean;
-}
+};
 
 export type ContactDisplayNameOptions = {
     no_jid?: boolean; // If true, null will be returned instead of the JID
     context?: 'roster'; // The context in which the display name is being requested
-}
+};

+ 3 - 4
src/headless/plugins/roster/utils.js

@@ -168,9 +168,8 @@ export async function onStatusInitialized(reconnecting) {
         const id = `converse.presences-${bare_jid}`;
 
         initStorage(presences, id, 'session');
-        // We might be continuing an existing session, so we fetch
-        // cached presence data.
-        presences.fetch();
+        // We might be continuing an existing session, so we fetch cached presence data.
+        await new Promise((r) => presences.fetch({ success: r, error: r }));
     }
     /**
      * Triggered once the _converse.Presences collection has been
@@ -179,7 +178,7 @@ export async function onStatusInitialized(reconnecting) {
      * Converse having reconnected.
      * @event _converse#presencesInitialized
      * @type {boolean}
-     * @example _converse.api.listen.on('presencesInitialized', reconnecting => { ... });
+     * @example _converse.api.listen.on('presencesInitialized', (reconnecting) => { ... });
      */
     api.trigger('presencesInitialized', reconnecting);
 }

+ 53 - 46
src/headless/plugins/smacks/tests/smacks.js

@@ -1,5 +1,6 @@
 /*global mock, converse */
 
+const { stx } = converse.env;
 const $iq = converse.env.$iq;
 const $msg = converse.env.$msg;
 const Strophe = converse.env.Strophe;
@@ -8,6 +9,8 @@ const u = converse.env.utils;
 
 describe("XEP-0198 Stream Management", function () {
 
+    beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
+
     it("gets enabled with an <enable> stanza and resumed with a <resume> stanza",
         mock.initConverse(
             ['chatBoxesInitialized'],
@@ -27,7 +30,7 @@ describe("XEP-0198 Stream Management", function () {
         expect(_converse.session.get('smacks_enabled')).toBe(false);
         expect(Strophe.serialize(stanza)).toEqual('<enable resume="true" xmlns="urn:xmpp:sm:3"/>');
 
-        let result = u.toStanza(`<enabled xmlns="urn:xmpp:sm:3" id="some-long-sm-id" resume="true"/>`);
+        let result = stx`<enabled xmlns="urn:xmpp:sm:3" id="some-long-sm-id" resume="true"/>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(result));
         expect(_converse.session.get('smacks_enabled')).toBe(true);
 
@@ -42,24 +45,26 @@ describe("XEP-0198 Stream Management", function () {
         await u.waitUntil(() => IQ_stanzas.length === 5);
 
         const disco_iq = IQ_stanzas[0];
-        expect(Strophe.serialize(disco_iq)).toBe(
-            `<iq from="romeo@montague.lit/orchard" id="${disco_iq.getAttribute('id')}" to="montague.lit" type="get" xmlns="jabber:client">`+
-                `<query xmlns="http://jabber.org/protocol/disco#info"/></iq>`);
+        expect(disco_iq).toEqualStanza(stx`
+            <iq from="romeo@montague.lit/orchard" id="${disco_iq.getAttribute('id')}" to="montague.lit" type="get" xmlns="jabber:client">
+                <query xmlns="http://jabber.org/protocol/disco#info"/></iq>`);
 
-        expect(Strophe.serialize(IQ_stanzas[1])).toBe(
-            `<iq id="${IQ_stanzas[1].getAttribute('id')}" type="get" xmlns="jabber:client"><query xmlns="jabber:iq:roster"/></iq>`);
+        expect(IQ_stanzas[1]).toEqualStanza(stx`
+            <iq id="${IQ_stanzas[1].getAttribute('id')}" type="get" xmlns="jabber:client">
+                <query xmlns="jabber:iq:roster"/></iq>`);
         await mock.waitForRoster(_converse, 'current', 1);
 
-        expect(Strophe.serialize(IQ_stanzas[2])).toBe(
-            `<iq from="romeo@montague.lit/orchard" id="${IQ_stanzas[2].getAttribute('id')}" to="romeo@montague.lit" type="get" xmlns="jabber:client">`+
-                `<query xmlns="http://jabber.org/protocol/disco#info"/></iq>`);
+        expect(IQ_stanzas[2]).toEqualStanza(stx`
+            <iq from="romeo@montague.lit" id="${IQ_stanzas[2].getAttribute('id')}" to="romeo@montague.lit" type="get" xmlns="jabber:client">
+            <pubsub xmlns="http://jabber.org/protocol/pubsub"><items node="eu.siacs.conversations.axolotl.devicelist"/></pubsub></iq>`);
 
-        expect(Strophe.serialize(IQ_stanzas[3])).toBe(
-            `<iq from="romeo@montague.lit" id="${IQ_stanzas[3].getAttribute('id')}" to="romeo@montague.lit" type="get" xmlns="jabber:client">`+
-            `<pubsub xmlns="http://jabber.org/protocol/pubsub"><items node="eu.siacs.conversations.axolotl.devicelist"/></pubsub></iq>`);
+        expect(IQ_stanzas[3]).toEqualStanza(stx`
+            <iq from="romeo@montague.lit/orchard" id="${IQ_stanzas[3].getAttribute('id')}" to="romeo@montague.lit" type="get" xmlns="jabber:client">
+                <query xmlns="http://jabber.org/protocol/disco#info"/></iq>`);
 
-        expect(Strophe.serialize(IQ_stanzas[4])).toBe(
-            `<iq from="romeo@montague.lit/orchard" id="${IQ_stanzas[4].getAttribute('id')}" type="set" xmlns="jabber:client"><enable xmlns="urn:xmpp:carbons:2"/></iq>`);
+        expect(IQ_stanzas[4]).toEqualStanza(stx`
+            <iq from="romeo@montague.lit/orchard" id="${IQ_stanzas[4].getAttribute('id')}" type="set" xmlns="jabber:client">
+                <enable xmlns="urn:xmpp:carbons:2"/></iq>`);
 
         await u.waitUntil(() => sent_stanzas.filter(s => (s.nodeName === 'presence')).length);
 
@@ -67,33 +72,29 @@ describe("XEP-0198 Stream Management", function () {
         expect(_converse.session.get('unacked_stanzas').length).toBe(6);
 
         // test handling of acks
-        let ack = u.toStanza(`<a xmlns="urn:xmpp:sm:3" h="2"/>`);
+        let ack = stx`<a xmlns="urn:xmpp:sm:3" h="2"/>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(ack));
         expect(_converse.session.get('unacked_stanzas').length).toBe(4);
 
         // test handling of ack requests
-        let r = u.toStanza(`<r xmlns="urn:xmpp:sm:3"/>`);
+        let r = stx`<r xmlns="urn:xmpp:sm:3"/>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(r));
 
         // "h" is 3 because we received two IQ responses, for disco and the roster
         ack = await u.waitUntil(() => sent_stanzas.filter(s => (s.nodeName === 'a')).pop());
         expect(Strophe.serialize(ack)).toBe('<a h="2" xmlns="urn:xmpp:sm:3"/>');
 
-        const disco_result = $iq({
-            'type': 'result',
-            'from': 'montague.lit',
-            'to': 'romeo@montague.lit/orchard',
-            'id': disco_iq.getAttribute('id'),
-        }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
-            .c('identity', {
-                'category': 'server',
-                'type': 'im'
-            }).up()
-            .c('feature', {'var': 'http://jabber.org/protocol/disco#info'}).up()
-            .c('feature', {'var': 'http://jabber.org/protocol/disco#items'});
+        const disco_result = stx`
+            <iq xmlns="jabber:client" type="result" from="montague.lit" to="romeo@montague.lit/orchard" id="${disco_iq.getAttribute('id')}">
+                <query xmlns="http://jabber.org/protocol/disco#info">
+                    <identity category="server" type="im"/>
+                    <feature var="http://jabber.org/protocol/disco#info"/>
+                    <feature var="http://jabber.org/protocol/disco#items"/>
+                </query>
+            </iq>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(disco_result));
 
-        ack = u.toStanza(`<a xmlns="urn:xmpp:sm:3" h="2"/>`);
+        ack = stx`<a xmlns="urn:xmpp:sm:3" h="2"/>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(ack));
         expect(_converse.session.get('unacked_stanzas').length).toBe(4);
 
@@ -105,7 +106,7 @@ describe("XEP-0198 Stream Management", function () {
                 `<c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>`+
             `</presence>`);
 
-        r = u.toStanza(`<r xmlns="urn:xmpp:sm:3"/>`);
+        r = stx`<r xmlns="urn:xmpp:sm:3"/>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(r));
 
         ack = await u.waitUntil(() => sent_stanzas.filter(s => (s.nodeName === 'a' && s.getAttribute('h') === '3')).pop());
@@ -120,7 +121,7 @@ describe("XEP-0198 Stream Management", function () {
         stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'resume')).pop(), 1000);
         expect(Strophe.serialize(stanza)).toEqual('<resume h="3" previd="some-long-sm-id" xmlns="urn:xmpp:sm:3"/>');
 
-        result = u.toStanza(`<resumed xmlns="urn:xmpp:sm:3" h="another-sequence-number" previd="some-long-sm-id"/>`);
+        result = stx`<resumed xmlns="urn:xmpp:sm:3" h="another-sequence-number" previd="some-long-sm-id"/>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(result));
 
         // Another <enable> stanza doesn't get sent out
@@ -132,18 +133,24 @@ describe("XEP-0198 Stream Management", function () {
 
         // Test that unacked stanzas get resent out
         let iq = IQ_stanzas.pop();
-        expect(Strophe.serialize(iq)).toBe(
-            `<iq from="romeo@montague.lit/orchard" id="${iq.getAttribute('id')}" type="set" xmlns="jabber:client"><enable xmlns="urn:xmpp:carbons:2"/></iq>`);
+        expect(iq).toEqualStanza(stx`
+            <iq from="romeo@montague.lit/orchard" id="${iq.getAttribute('id')}" type="set" xmlns="jabber:client">
+                <enable xmlns="urn:xmpp:carbons:2"/>
+            </iq>`);
 
         iq = IQ_stanzas.pop();
-        expect(Strophe.serialize(iq)).toBe(
-            `<iq from="romeo@montague.lit" id="${iq.getAttribute('id')}" to="romeo@montague.lit" type="get" xmlns="jabber:client">`+
-            `<pubsub xmlns="http://jabber.org/protocol/pubsub"><items node="eu.siacs.conversations.axolotl.devicelist"/></pubsub></iq>`);
+        expect(iq).toEqualStanza(stx`
+            <iq from="romeo@montague.lit/orchard" id="${iq.getAttribute('id')}" to="romeo@montague.lit" type="get" xmlns="jabber:client">
+                <query xmlns="http://jabber.org/protocol/disco#info"/>
+            </iq>`);
 
         iq = IQ_stanzas.pop();
-        expect(Strophe.serialize(iq)).toBe(
-            `<iq from="romeo@montague.lit/orchard" id="${iq.getAttribute('id')}" to="romeo@montague.lit" type="get" xmlns="jabber:client">`+
-                `<query xmlns="http://jabber.org/protocol/disco#info"/></iq>`);
+        expect(iq).toEqualStanza(stx`
+            <iq from="romeo@montague.lit" id="${iq.getAttribute('id')}" to="romeo@montague.lit" type="get" xmlns="jabber:client">
+                <pubsub xmlns="http://jabber.org/protocol/pubsub">
+                    <items node="eu.siacs.conversations.axolotl.devicelist"/>
+                </pubsub>
+            </iq>`);
 
         expect(IQ_stanzas.filter(iq => sizzle('query[xmlns="jabber:iq:roster"]', iq).pop()).length).toBe(0);
     }));
@@ -163,7 +170,7 @@ describe("XEP-0198 Stream Management", function () {
         const sent_stanzas = _converse.api.connection.get().sent_stanzas;
         let stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'enable')).pop());
         expect(Strophe.serialize(stanza)).toEqual('<enable resume="true" xmlns="urn:xmpp:sm:3"/>');
-        let result = u.toStanza(`<enabled xmlns="urn:xmpp:sm:3" id="some-long-sm-id" resume="true"/>`);
+        let result = stx`<enabled xmlns="urn:xmpp:sm:3" id="some-long-sm-id" resume="true"/>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(result));
 
         await mock.waitForRoster(_converse, 'current', 1);
@@ -173,10 +180,10 @@ describe("XEP-0198 Stream Management", function () {
         stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'resume')).pop());
         expect(Strophe.serialize(stanza)).toEqual('<resume h="1" previd="some-long-sm-id" xmlns="urn:xmpp:sm:3"/>');
 
-        result = u.toStanza(
-            `<failed xmlns="urn:xmpp:sm:3" h="another-sequence-number">`+
-                `<item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>`+
-            `</failed>`);
+        result = stx`
+            <failed xmlns="urn:xmpp:sm:3" h="another-sequence-number">
+                <item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
+            </failed>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(result));
 
         // Session data gets reset
@@ -192,7 +199,7 @@ describe("XEP-0198 Stream Management", function () {
         stanza = sent_stanzas.filter(s => (s.tagName === 'enable')).pop();
         expect(Strophe.serialize(stanza)).toEqual('<enable resume="true" xmlns="urn:xmpp:sm:3"/>');
 
-        result = u.toStanza(`<enabled xmlns="urn:xmpp:sm:3" id="another-long-sm-id" resume="true"/>`);
+        result = stx`<enabled xmlns="urn:xmpp:sm:3" id="another-long-sm-id" resume="true"/>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(result));
         expect(_converse.session.get('smacks_enabled')).toBe(true);
 
@@ -272,7 +279,7 @@ describe("XEP-0198 Stream Management", function () {
         const stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'resume')).pop());
         expect(Strophe.serialize(stanza)).toEqual('<resume h="580" previd="some-long-sm-id" xmlns="urn:xmpp:sm:3"/>');
 
-        const result = u.toStanza(`<resumed xmlns="urn:xmpp:sm:3" h="another-sequence-number" previd="some-long-sm-id"/>`);
+        const result = stx`<resumed xmlns="urn:xmpp:sm:3" h="another-sequence-number" previd="some-long-sm-id"/>`;
         api.connection.get()._dataRecv(mock.createRequest(result));
         expect(_converse.session.get('smacks_enabled')).toBe(true);
 

+ 0 - 1
src/headless/shared/connection/index.js

@@ -108,7 +108,6 @@ export class Connection extends Strophe.Connection {
     /**
      * Establish a new XMPP session by logging in with the supplied JID and
      * password.
-     * @method Connnection.connect
      * @param {String} jid
      * @param {String} password
      * @param {Function} callback

+ 4 - 0
src/headless/types/plugins/chat/model.d.ts

@@ -369,6 +369,10 @@ declare class ChatBox extends ChatBox_base {
         resolve: (value: any) => void;
         reject: (reason?: any) => void;
     };
+    /**
+     * @param {string} jid
+     */
+    setPresence(jid: string): Promise<void>;
     presence: any;
     /**
      * @param {MessageAttributes|StanzaParseError} attrs_or_error

+ 1 - 1
src/headless/types/plugins/roster/contact.d.ts

@@ -151,7 +151,7 @@ declare class RosterContact extends RosterContact_base {
         resolve: (value: any) => void;
         reject: (reason?: any) => void;
     };
-    setPresence(): void;
+    setPresence(): Promise<void>;
     presence: any;
     getStatus(): any;
     openChat(): void;

+ 6 - 0
src/headless/types/plugins/roster/parsers.d.ts

@@ -0,0 +1,6 @@
+/**
+ * @param {Element} stanza
+ * @returns {import('./types').Presence}
+ */
+export function parsePresence(stanza: Element): import("./types").Presence;
+//# sourceMappingURL=parsers.d.ts.map

+ 7 - 6
src/headless/types/plugins/roster/presence.d.ts

@@ -1,32 +1,33 @@
 export default Presence;
 declare class Presence extends Model {
     defaults(): {
-        show: string;
+        presence: string;
+        show: any;
     };
     initialize(): void;
     resources: Resources;
     onResourcesChanged(): void;
+    getStatus(): any;
     /**
      * Return the resource with the highest priority.
      * If multiple resources have the same priority, take the latest one.
-     * @private
      */
-    private getHighestPriorityResource;
+    getHighestPriorityResource(): any;
     /**
      * Adds a new resource and it's associated attributes as taken
      * from the passed in presence stanza.
      * Also updates the presence if the resource has higher priority (and is newer).
-     * @param { Element } presence: The presence stanza
+     * @param {Element} presence: The presence stanza
      */
     addResource(presence: Element): void;
     /**
      * Remove the passed in resource from the resources map.
      * Also redetermines the presence given that there's one less
      * resource.
-     * @param { string } name: The resource name
+     * @param {string} name: The resource name
      */
     removeResource(name: string): void;
 }
 import { Model } from '@converse/skeletor';
-import Resources from "./resources.js";
+import Resources from './resources.js';
 //# sourceMappingURL=presence.d.ts.map

+ 10 - 1
src/headless/types/plugins/roster/types.d.ts

@@ -1,10 +1,19 @@
+export type PresenceTypes = null | 'available' | 'unavailable' | 'error' | 'probe' | 'subscribe' | 'subscribed' | 'unsubscribe' | 'unsubscribed';
+export type PresenceShowValues = 'chat' | 'away' | 'dnd' | 'xa';
+export type Presence = {
+    resource: string;
+    type: PresenceTypes;
+    priority: Number;
+    show?: PresenceShowValues;
+    timestamp: string;
+};
 export type RosterContactUpdateAttrs = {
     nickname?: string;
     groups?: string[];
 };
 export type RosterContactAttributes = {
     jid: string;
-    subscription: ('none' | 'to' | 'from' | 'both');
+    subscription: 'none' | 'to' | 'from' | 'both';
     ask?: 'subscribe';
     name?: string;
     groups?: string[];

+ 0 - 1
src/headless/types/shared/connection/index.d.ts

@@ -24,7 +24,6 @@ export class Connection extends Connection_base {
     /**
      * Establish a new XMPP session by logging in with the supplied JID and
      * password.
-     * @method Connnection.connect
      * @param {String} jid
      * @param {String} password
      * @param {Function} callback

+ 0 - 25
src/plugins/controlbox/tests/controlbox.js

@@ -33,31 +33,6 @@ describe("The Controlbox", function () {
 
     describe("The \"Contacts\" section", function () {
 
-        it("can be used to add contact and it checks for case-sensivity",
-                mock.initConverse([], { show_self_in_roster: false }, async function (_converse) {
-
-            spyOn(_converse.api, "trigger").and.callThrough();
-            await mock.waitForRoster(_converse, 'all', 0);
-            await mock.openControlBox(_converse);
-            // Adding two contacts one with Capital initials and one with small initials of same JID (Case sensitive check)
-            _converse.roster.create({
-                jid: mock.pend_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
-                subscription: 'none',
-                ask: 'subscribe',
-                fullname: mock.pend_names[0]
-            });
-            _converse.roster.create({
-                jid: mock.pend_names[0].replace(/ /g,'.') + '@montague.lit',
-                subscription: 'none',
-                ask: 'subscribe',
-                fullname: mock.pend_names[0]
-            });
-            const rosterview = await u.waitUntil(() => document.querySelector('converse-roster'));
-            await u.waitUntil(() => Array.from(rosterview.querySelectorAll('.roster-group li')).filter(u.isVisible).length, 700);
-            // Checking that only one entry is created because both JID is same (Case sensitive check)
-            expect(Array.from(rosterview.querySelectorAll('li')).filter(u.isVisible).length).toBe(1);
-        }));
-
         it("shows the number of unread mentions received",
                 mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
 

+ 1 - 1
src/plugins/rosterview/contactview.js

@@ -32,7 +32,7 @@ export default class RosterContactView extends ObservableElement {
         this.listenTo(this.model, 'highlight', () => this.requestUpdate());
         this.listenTo(this.model, 'vcard:add', () => this.requestUpdate());
         this.listenTo(this.model, 'vcard:change', () => this.requestUpdate());
-        this.listenTo(this.model, 'presenceChanged', () => this.requestUpdate());
+        this.listenTo(this.model, 'presence:change', () => this.requestUpdate());
     }
 
     render() {

+ 1 - 1
src/plugins/rosterview/templates/group.js

@@ -56,7 +56,7 @@ export default (o) => {
     return html`
         <div class="roster-group" data-group="${o.name}">
             <a href="#" class="list-toggle group-toggle controlbox-padded" title="${i18n_title}" @click=${ev => toggleGroup(ev, o.name)}>
-                <converse-icon color="var(--background-color)" size="1em" class="fa ${ (collapsed.includes(o.name)) ? 'fa-caret-right' : 'fa-caret-down' }"></converse-icon> ${o.name}
+                <converse-icon color="var(--chat-color)" size="1em" class="fa ${ (collapsed.includes(o.name)) ? 'fa-caret-right' : 'fa-caret-down' }"></converse-icon> ${o.name}
             </a>
             <ul class="items-list roster-group-contacts ${ (collapsed.includes(o.name)) ? 'collapsed' : '' }" data-group="${o.name}">
                 ${ repeat(o.contacts, (c) => c.get('jid'), renderContact) }

+ 1 - 1
src/plugins/rosterview/templates/roster_item.js

@@ -13,7 +13,7 @@ export function tplRemoveButton(el) {
     return html`<a
         class="dropdown-item remove-xmpp-contact"
         role="button"
-        @click="${el.removeContact}"
+        @click="${(ev) => el.removeContact(ev)}"
         title="${i18n_remove}"
         data-toggle="modal"
     >

+ 4 - 4
src/plugins/rosterview/tests/protocol.js

@@ -176,7 +176,7 @@ describe("Presence subscriptions", function () {
             }, 600);
 
             let header = sizzle('a:contains("My Buddies")', rosterview).pop();
-            let contacts = header.parentElement.querySelectorAll('li');
+            let contacts = header.parentElement.querySelectorAll('li .open-chat');
             expect(contacts.length).toBe(1);
             expect(u.isVisible(contacts[0])).toBe(true);
             sent_stanza = ""; // Reset
@@ -226,7 +226,7 @@ describe("Presence subscriptions", function () {
             header = sizzle('a:contains("My contacts")', rosterview);
             expect(header.length).toBe(1);
             expect(u.isVisible(header[0])).toBeTruthy();
-            contacts = header[0].parentNode.querySelectorAll('li');
+            contacts = header[0].parentNode.querySelectorAll('li.current-xmpp-contact');
             expect(contacts.length).toBe(1);
             // Check that it has the right classes and text
             expect(u.hasClass('to', contacts[0])).toBeTruthy();
@@ -235,7 +235,7 @@ describe("Presence subscriptions", function () {
 
             await u.waitUntil(() => contacts[0].querySelector('.contact-name')?.textContent.trim() === 'Nicky');
 
-            expect(contact.presence.get('show')).toBe('offline');
+            expect(contact.presence.getStatus()).toBe('offline');
 
             /*  <presence
              *      from='contact@example.org/resource'
@@ -244,7 +244,7 @@ describe("Presence subscriptions", function () {
             stanza = $pres({'to': _converse.bare_jid, 'from': 'contact@example.org/resource'});
             _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
             // Now the contact should also be online.
-            expect(contact.presence.get('show')).toBe('online');
+            expect(contact.presence.getStatus()).toBe('online');
 
             /* Section 8.3.  Creating a Mutual Subscription
              *

+ 26 - 19
src/plugins/rosterview/tests/roster.js

@@ -16,8 +16,8 @@ const checkHeaderToggling = async function (group) {
     expect(u.hasClass('fa-caret-right', toggle.firstElementChild)).toBeTruthy();
     expect(u.hasClass('fa-caret-down', toggle.firstElementChild)).toBeFalsy();
     toggle.click();
-    await u.waitUntil(() => group.querySelectorAll('li').length ===
-        Array.from(group.querySelectorAll('li')).filter(u.isVisible).length);
+    await u.waitUntil(() => group.querySelectorAll('li .open-chat').length ===
+        Array.from(group.querySelectorAll('li .open-chat')).filter(u.isVisible).length);
 
     expect(u.hasClass('fa-caret-right', toggle.firstElementChild)).toBeFalsy();
     expect(u.hasClass('fa-caret-down', toggle.firstElementChild)).toBeTruthy();
@@ -361,7 +361,7 @@ describe("The Contacts Roster", function () {
             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', roster).filter(l => !u.isVisible(l)).length).toBe(0);
+            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";
@@ -408,9 +408,11 @@ describe("The Contacts Roster", function () {
             await mock.waitForRoster(_converse, 'all');
 
             let jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            _converse.roster.get(jid).presence.set('show', 'online');
+            _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');
+            _converse.roster.get(jid).presence.set({ show: 'dnd', presence: 'online' });
+
             await mock.openControlBox(_converse);
             const rosterview = document.querySelector('converse-roster');
 
@@ -430,8 +432,8 @@ describe("The Contacts Roster", function () {
 
             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');
@@ -491,7 +493,7 @@ describe("The Contacts Roster", function () {
                 "ænemies",
                 "Ungrouped",
             ]);
-            const contacts = sizzle('.roster-group[data-group="New messages"] li', rosterview);
+            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");
@@ -511,7 +513,7 @@ describe("The Contacts Roster", function () {
 
         it("can be used to organize existing contacts",
             mock.initConverse(
-                [], {'roster_groups': true},
+                [], { roster_groups: true, show_self_in_roster: false },
                 async function (_converse) {
 
             await mock.openControlBox(_converse);
@@ -530,7 +532,7 @@ describe("The Contacts Roster", function () {
             ]);
             // Check that usernames appear alphabetically per group
             Object.keys(mock.groups).forEach(name  => {
-                const contacts = sizzle('.roster-group[data-group="'+name+'"] ul', rosterview);
+                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();
@@ -597,7 +599,7 @@ describe("The Contacts Roster", function () {
             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', rosterview);
+                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();
@@ -806,6 +808,7 @@ describe("The Contacts Roster", function () {
             pres = $pres({from: 'mercutio@montague.lit/resource'}).c('show', 'away');
             _converse.api.connection.get()._dataRecv(mock.createRequest(pres));
             await u.waitUntil(() => icon_el.getAttribute('color') === 'var(--chat-status-away)');
+                    return
 
             pres = $pres({from: 'mercutio@montague.lit/resource'}).c('show', 'xa');
             _converse.api.connection.get()._dataRecv(mock.createRequest(pres));
@@ -1046,23 +1049,23 @@ describe("The Contacts Roster", function () {
             let i, jid;
             for (i=0; i<3; i++) {
                 jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-                _converse.roster.get(jid).presence.set('show', 'online');
+                _converse.roster.get(jid).presence.set('presence', 'online');
             }
             for (i=3; i<6; i++) {
                 jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-                _converse.roster.get(jid).presence.set('show', 'dnd');
+                _converse.roster.get(jid).presence.set({ presence: 'online', show: 'dnd' });
             }
             for (i=6; i<9; i++) {
                 jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-                _converse.roster.get(jid).presence.set('show', 'away');
+                _converse.roster.get(jid).presence.set({ presence: 'online', show: 'away' });
             }
             for (i=9; i<12; i++) {
                 jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-                _converse.roster.get(jid).presence.set('show', 'xa');
+                _converse.roster.get(jid).presence.set({ presence: 'online', show: 'xa' });
             }
             for (i=12; i<15; i++) {
                 jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-                _converse.roster.get(jid).presence.set('show', 'unavailable');
+                _converse.roster.get(jid).presence.set('presence', 'offline');
             }
 
             await u.waitUntil(() => u.isVisible(rosterview.querySelector('li.list-item:first-child')));
@@ -1089,13 +1092,15 @@ describe("The Contacts Roster", function () {
                     expect(statuses.join(" ")).toBe("online online away xa xa xa");
                     expect(status_classes.join(" ")).toBe("online online away xa xa xa");
                     expect(subscription_classes.join(" ")).toBe("both both both both both both");
+
                 } else if (groupname === "friends & acquaintences") {
                     const statuses = els.map(e => e.getAttribute('data-status'));
                     const subscription_classes = els.map(e => e.classList[4]);
                     const status_classes = els.map(e => e.classList[5]);
-                    expect(statuses.join(" ")).toBe("online online dnd dnd away unavailable");
-                    expect(status_classes.join(" ")).toBe("online online dnd dnd away unavailable");
+                    expect(statuses.join(" ")).toBe("online online dnd dnd away offline");
+                    expect(status_classes.join(" ")).toBe("online online dnd dnd away offline");
                     expect(subscription_classes.join(" ")).toBe("both both both both both both");
+
                 } else if (groupname === "Family") {
                     const statuses = els.map(e => e.getAttribute('data-status'));
                     const subscription_classes = els.map(e => e.classList[4]);
@@ -1103,6 +1108,7 @@ describe("The Contacts Roster", function () {
                     expect(statuses.join(" ")).toBe("online dnd");
                     expect(status_classes.join(" ")).toBe("online dnd");
                     expect(subscription_classes.join(" ")).toBe("both both");
+
                 } else if (groupname === "ænemies") {
                     const statuses = els.map(e => e.getAttribute('data-status'));
                     const subscription_classes = els.map(e => e.classList[4]);
@@ -1110,12 +1116,13 @@ describe("The Contacts Roster", function () {
                     expect(statuses.join(" ")).toBe("away");
                     expect(status_classes.join(" ")).toBe("away");
                     expect(subscription_classes.join(" ")).toBe("both");
+
                 } else if (groupname === "Ungrouped") {
                     const statuses = els.map(e => e.getAttribute('data-status'));
                     const subscription_classes = els.map(e => e.classList[4]);
                     const status_classes = els.map(e => e.classList[5]);
-                    expect(statuses.join(" ")).toBe("unavailable unavailable");
-                    expect(status_classes.join(" ")).toBe("unavailable unavailable");
+                    expect(statuses.join(" ")).toBe("offline offline");
+                    expect(status_classes.join(" ")).toBe("offline offline");
                     expect(subscription_classes.join(" ")).toBe("both both");
                 }
             }

+ 6 - 6
src/plugins/rosterview/tests/unsaved-contacts.js

@@ -21,8 +21,8 @@ describe('An unsaved Contact', function () {
             await _converse.handleMessageStanza(msg);
 
             const rosterview = document.querySelector('converse-roster');
-            await u.waitUntil(() => rosterview.querySelectorAll(`ul[data-group="Unsaved contacts"] li`).length);
-            expect(rosterview.querySelectorAll(`ul[data-group="Unsaved contacts"] li`).length).toBe(1);
+            await u.waitUntil(() => rosterview.querySelectorAll(`ul[data-group="Unsaved contacts"] li .open-chat`).length);
+            expect(rosterview.querySelectorAll(`ul[data-group="Unsaved contacts"] li .open-chat`).length).toBe(1);
             const el = rosterview.querySelector(`ul[data-group="Unsaved contacts"] li .contact-name`);
             expect(el.textContent).toBe('Mercutio');
         })
@@ -63,8 +63,8 @@ describe('An unsaved Contact', function () {
                 </message>`;
             await _converse.handleMessageStanza(msg);
 
-            await u.waitUntil(() => rosterview.querySelectorAll(`ul[data-group="Unsaved contacts"] li`).length);
-            expect(rosterview.querySelectorAll(`ul[data-group="Unsaved contacts"] li`).length).toBe(1);
+            await u.waitUntil(() => rosterview.querySelectorAll(`ul[data-group="Unsaved contacts"] li .open-chat`).length);
+            expect(rosterview.querySelectorAll(`ul[data-group="Unsaved contacts"] li .open-chat`).length).toBe(1);
             const el = rosterview.querySelector(`ul[data-group="Unsaved contacts"] li .contact-name`);
             expect(el.textContent).toBe('Mercutio');
         })
@@ -90,8 +90,8 @@ describe('An unsaved Contact', function () {
             await _converse.handleMessageStanza(msg);
 
             const rosterview = document.querySelector('converse-roster');
-            await u.waitUntil(() => rosterview.querySelectorAll(`ul[data-group="Unsaved contacts"] li`).length);
-            expect(rosterview.querySelectorAll(`ul[data-group="Unsaved contacts"] li`).length).toBe(1);
+            await u.waitUntil(() => rosterview.querySelectorAll(`ul[data-group="Unsaved contacts"] li .open-chat`).length);
+            expect(rosterview.querySelectorAll(`ul[data-group="Unsaved contacts"] li .open-chat`).length).toBe(1);
             const el = rosterview.querySelector(`ul[data-group="Unsaved contacts"] li .contact-name`);
             expect(el.textContent).toBe('Mercutio');