Explorar el Código

Clear unread message counters when chat becomes visible

JC Brand hace 4 años
padre
commit
d0627f800c

+ 0 - 1
Makefile

@@ -209,7 +209,6 @@ eslint: node_modules
 	$(ESLINT) src/utils/*.js
 	$(ESLINT) src/headless/*.js
 	$(ESLINT) src/headless/utils/*.js
-	$(ESLINT) spec/ --global converse
 
 .PHONY: check
 check: eslint | dist/converse.js dist/converse.css

+ 2 - 2
karma.conf.js

@@ -22,13 +22,13 @@ module.exports = function(config) {
         served: true,
         nocache: false
       },
-      { pattern: "node_modules/sinon/pkg/sinon.js", type: 'module' },
-      { pattern: "spec/mock.js", type: 'module' },
+      { pattern: "src/shared/tests/mock.js", type: 'module' },
 
       { pattern: "src/headless/plugins/caps/tests/caps.js", type: 'module' },
       { pattern: "src/headless/plugins/chat/tests/api.js", type: 'module' },
       { pattern: "src/headless/plugins/disco/tests/disco.js", type: 'module' },
       { pattern: "src/headless/plugins/muc/tests/affiliations.js", type: 'module' },
+      { pattern: "src/headless/plugins/muc/tests/muc.js", type: 'module' },
       { pattern: "src/headless/plugins/muc/tests/pruning.js", type: 'module' },
       { pattern: "src/headless/plugins/muc/tests/registration.js", type: 'module' },
       { pattern: "src/headless/plugins/ping/tests/ping.js", type: 'module' },

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

@@ -1066,8 +1066,7 @@ const ChatBox = ModelWithContact.extend({
      */
     isHidden () {
         // Note: This methods gets overridden by converse-minimize
-        const hidden = _converse.isUniView() && this.get('hidden');
-        return hidden || this.isScrolledUp() || _converse.windowState === 'hidden';
+        return this.get('hidden') || this.isScrolledUp() || _converse.windowState === 'hidden';
     },
 
     /**
@@ -1088,21 +1087,25 @@ const ChatBox = ModelWithContact.extend({
                 // when the user writes a message as opposed to when a
                 // message is received.
                 this.ui.set('scrolled', false);
-            } else if (this.isHidden() || this.ui.get('scrolled')) {
-                const settings = {
-                    'num_unread': this.get('num_unread') + 1
-                };
-                if (this.get('num_unread') === 0) {
-                    settings['first_unread_id'] = message.get('id');
-                }
-                this.save(settings);
+            } else if (this.isHidden()) {
+                this.incrementUnreadMsgsCounter(message);
             } else {
                 this.sendMarkerForMessage(message);
             }
         }
     },
 
-    clearUnreadMsgCounter() {
+    incrementUnreadMsgsCounter (message) {
+        const settings = {
+            'num_unread': this.get('num_unread') + 1
+        };
+        if (this.get('num_unread') === 0) {
+            settings['first_unread_id'] = message.get('id');
+        }
+        this.save(settings);
+    },
+
+    clearUnreadMsgCounter () {
         if (this.get('num_unread') > 0) {
             this.sendMarkerForMessage(this.messages.last());
         }

+ 18 - 35
src/headless/plugins/muc/muc.js

@@ -257,16 +257,21 @@ const ChatRoomMixin = {
      */
     async onHiddenChange () {
         const conn_status = this.session.get('connection_status');
-        if (this.get('hidden') && conn_status === converse.ROOMSTATUS.ENTERED) {
-            if (api.settings.get('muc_subscribe_to_rai') && this.getOwnAffiliation() !== 'none') {
+        if (this.get('hidden')) {
+            if (conn_status === converse.ROOMSTATUS.ENTERED &&
+                    api.settings.get('muc_subscribe_to_rai') &&
+                    this.getOwnAffiliation() !== 'none') {
                 if (conn_status !== converse.ROOMSTATUS.DISCONNECTED) {
                     this.sendMarkerForLastMessage('received', true);
                     await this.leave();
                 }
                 this.enableRAI();
             }
-        } else if (conn_status === converse.ROOMSTATUS.DISCONNECTED) {
-            this.rejoin();
+        } else {
+            if (conn_status === converse.ROOMSTATUS.DISCONNECTED) {
+                this.rejoin();
+            }
+            this.clearUnreadMsgCounter();
         }
     },
 
@@ -2570,39 +2575,17 @@ const ChatRoomMixin = {
         }
     },
 
-    /**
-     * Given a newly received {@link _converse.Message} instance,
-     * update the unread counter if necessary.
-     * @private
-     * @method _converse.ChatRoom#handleUnreadMessage
-     * @param { XMLElement } - The <messsage> stanza
-     */
-    handleUnreadMessage (message) {
-        if (!message?.get('body')) {
-            return;
+    incrementUnreadMsgsCounter (message) {
+        const settings = {
+            'num_unread_general': this.get('num_unread_general') + 1
+        };
+        if (this.get('num_unread_general') === 0) {
+            settings['first_unread_id'] = message.get('id');
         }
-        if (u.isNewMessage(message)) {
-            if (message.get('sender') === 'me') {
-                // We remove the "scrolled" flag so that the chat area
-                // gets scrolled down. We always want to scroll down
-                // when the user writes a message as opposed to when a
-                // message is received.
-                this.ui.set('scrolled', false);
-            } else if (this.isHidden() || this.ui.get('scrolled')) {
-                const settings = {
-                    'num_unread_general': this.get('num_unread_general') + 1
-                };
-                if (this.get('num_unread_general') === 0) {
-                    settings['first_unread_id'] = message.get('id');
-                }
-                if (this.isUserMentioned(message)) {
-                    settings.num_unread = this.get('num_unread') + 1;
-                }
-                this.save(settings);
-            } else {
-                this.sendMarkerForMessage(message);
-            }
+        if (this.isUserMentioned(message)) {
+            settings.num_unread = this.get('num_unread') + 1;
         }
+        this.save(settings);
     },
 
     clearUnreadMsgCounter () {

+ 39 - 0
src/headless/plugins/muc/tests/muc.js

@@ -0,0 +1,39 @@
+/*global mock, converse */
+
+const { u } = converse.env;
+
+describe("Groupchats", function () {
+
+    it("keeps track of unread messages and mentions",
+            mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
+
+        const nick = 'romeo';
+        const muc_jid = 'lounge@montague.lit';
+        // Open a hidden room
+        await mock.openAndEnterChatRoom(_converse, muc_jid, nick, [], [], false, {'hidden': true});
+        const model = _converse.chatboxes.get(muc_jid);
+
+        _converse.connection._dataRecv(mock.createRequest(u.toStanza(`
+            <message xmlns="jabber:client" type="groupchat" id="1" to="${_converse.jid}" xml:lang="en" from="${muc_jid}/juliet">
+                <body>Romeo oh romeo</body>
+            </message>`)));
+        await u.waitUntil(() => model.messages.length);
+        expect(model.get('num_unread_general')).toBe(1);
+        expect(model.get('num_unread')).toBe(1);
+
+        _converse.connection._dataRecv(mock.createRequest(u.toStanza(`
+            <message xmlns="jabber:client" type="groupchat" id="2" to="${_converse.jid}" xml:lang="en" from="${muc_jid}/juliet">
+                <body>Wherefore art though?</body>
+            </message>`)));
+
+        await u.waitUntil(() => model.messages.length === 2);
+
+        expect(model.get('num_unread_general')).toBe(2);
+        expect(model.get('num_unread')).toBe(1);
+
+        // Check that unread counters are cleared when chat becomes visible
+        model.set('hidden', false);
+        expect(model.get('num_unread_general')).toBe(0);
+        expect(model.get('num_unread')).toBe(0);
+    }));
+});

+ 0 - 49
src/plugins/muc-views/tests/muc.js

@@ -4584,55 +4584,6 @@ describe("Groupchats", function () {
         }));
     });
 
-    describe("The \"Groupchats\" section", function () {
-
-        it("shows the number of unread mentions received",
-            mock.initConverse(
-                [], {'allow_bookmarks': false},
-                async function (_converse) {
-
-            await mock.openControlBox(_converse);
-            const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
-            expect(roomspanel.querySelectorAll('.available-room').length).toBe(0);
-
-            const muc_jid = 'kitchen@conference.shakespeare.lit';
-            const message = 'fires: Your attention is required';
-            await mock.openAndEnterChatRoom(_converse, muc_jid, 'fires');
-            const view = _converse.chatboxviews.get(muc_jid);
-            await u.waitUntil(() => roomspanel.querySelectorAll('.available-room').length);
-            expect(roomspanel.querySelectorAll('.available-room').length).toBe(1);
-            expect(roomspanel.querySelectorAll('.msgs-indicator').length).toBe(0);
-
-            view.model.set({'minimized': true});
-
-            const nick = mock.chatroom_names[0];
-            await view.model.handleMessageStanza($msg({
-                    from: muc_jid+'/'+nick,
-                    id: u.getUniqueId(),
-                    to: 'romeo@montague.lit',
-                    type: 'groupchat'
-                }).c('body').t(message).tree());
-            await u.waitUntil(() => view.model.messages.length);
-            expect(roomspanel.querySelectorAll('.available-room').length).toBe(1);
-            expect(roomspanel.querySelectorAll('.msgs-indicator').length).toBe(1);
-            expect(roomspanel.querySelector('.msgs-indicator').textContent.trim()).toBe('1');
-
-            await view.model.handleMessageStanza($msg({
-                'from': muc_jid+'/'+nick,
-                'id': u.getUniqueId(),
-                'to': 'romeo@montague.lit',
-                'type': 'groupchat'
-            }).c('body').t(message).tree());
-            await u.waitUntil(() => view.model.messages.length > 1);
-            expect(roomspanel.querySelectorAll('.available-room').length).toBe(1);
-            expect(roomspanel.querySelectorAll('.msgs-indicator').length).toBe(1);
-            expect(roomspanel.querySelector('.msgs-indicator').textContent.trim()).toBe('2');
-            view.model.set({'minimized': false});
-            expect(roomspanel.querySelectorAll('.available-room').length).toBe(1);
-            expect(roomspanel.querySelectorAll('.msgs-indicator').length).toBe(0);
-        }));
-    });
-
     describe("A XEP-0085 Chat Status Notification", function () {
 
         it("is is not sent out to a MUC if the user is a visitor in a moderated room",

+ 47 - 1
src/plugins/muc-views/tests/muclist.js

@@ -1,6 +1,6 @@
 /* global mock, converse */
 
-const u = converse.env.utils;
+const { $msg, u } = converse.env;
 
 
 describe("A list of open groupchats", function () {
@@ -46,6 +46,52 @@ describe("A list of open groupchats", function () {
         expect(Array.from(list.classList).includes('hidden')).toBeTruthy();
     }));
 
+    it("shows the number of unread mentions received",
+        mock.initConverse(
+            [], {'allow_bookmarks': false},
+            async function (_converse) {
+
+        await mock.openControlBox(_converse);
+        const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
+        expect(roomspanel.querySelectorAll('.available-room').length).toBe(0);
+
+        const muc_jid = 'kitchen@conference.shakespeare.lit';
+        const message = 'fires: Your attention is required';
+        await mock.openAndEnterChatRoom(_converse, muc_jid, 'fires');
+        const view = _converse.chatboxviews.get(muc_jid);
+        await u.waitUntil(() => roomspanel.querySelectorAll('.available-room').length);
+        expect(roomspanel.querySelectorAll('.available-room').length).toBe(1);
+        expect(roomspanel.querySelectorAll('.msgs-indicator').length).toBe(0);
+
+        view.model.set({'minimized': true});
+
+        const nick = mock.chatroom_names[0];
+        await view.model.handleMessageStanza($msg({
+                from: muc_jid+'/'+nick,
+                id: u.getUniqueId(),
+                to: 'romeo@montague.lit',
+                type: 'groupchat'
+            }).c('body').t(message).tree());
+        await u.waitUntil(() => view.model.messages.length);
+        expect(roomspanel.querySelectorAll('.available-room').length).toBe(1);
+        expect(roomspanel.querySelectorAll('.msgs-indicator').length).toBe(1);
+        expect(roomspanel.querySelector('.msgs-indicator').textContent.trim()).toBe('1');
+
+        await view.model.handleMessageStanza($msg({
+            'from': muc_jid+'/'+nick,
+            'id': u.getUniqueId(),
+            'to': 'romeo@montague.lit',
+            'type': 'groupchat'
+        }).c('body').t(message).tree());
+        await u.waitUntil(() => view.model.messages.length > 1);
+        expect(roomspanel.querySelectorAll('.available-room').length).toBe(1);
+        expect(roomspanel.querySelectorAll('.msgs-indicator').length).toBe(1);
+        expect(roomspanel.querySelector('.msgs-indicator').textContent.trim()).toBe('2');
+        view.model.set({'minimized': false});
+        expect(roomspanel.querySelectorAll('.available-room').length).toBe(1);
+        expect(roomspanel.querySelectorAll('.msgs-indicator').length).toBe(0);
+    }));
+
     it("uses bookmarks to determine groupchat names",
         mock.initConverse(
             ['chatBoxesFetched'],

+ 144 - 104
spec/mock.js → src/shared/tests/mock.js

@@ -1,14 +1,12 @@
-const mock = {};
-window.mock = mock;
 let _converse;
-
+const mock = {};
 const converse = window.converse;
 converse.load();
 const { u, sizzle, Strophe, dayjs, $iq, $msg, $pres } = converse.env;
 
 jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000;
 
-mock.initConverse = function (promise_names=[], settings=null, func) {
+function initConverse (promise_names=[], settings=null, func) {
     if (typeof promise_names === "function") {
         func = promise_names;
         promise_names = []
@@ -25,7 +23,7 @@ mock.initConverse = function (promise_names=[], settings=null, func) {
         }
         document.title = "Converse Tests";
 
-        await initConverse(settings);
+        await _initConverse(settings);
         await Promise.all((promise_names || []).map(_converse.api.waitUntil));
         try {
             await func(_converse);
@@ -34,9 +32,9 @@ mock.initConverse = function (promise_names=[], settings=null, func) {
             fail(e);
         }
     }
-};
+}
 
-mock.waitUntilDiscoConfirmed = async function (_converse, entity_jid, identities, features=[], items=[], type='info') {
+async function waitUntilDiscoConfirmed (_converse, entity_jid, identities, features=[], items=[], type='info') {
     const sel = `iq[to="${entity_jid}"] query[xmlns="http://jabber.org/protocol/disco#${type}"]`;
     const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(iq => sizzle(sel, iq).length).pop(), 300);
     const stanza = $iq({
@@ -49,10 +47,10 @@ mock.waitUntilDiscoConfirmed = async function (_converse, entity_jid, identities
     identities?.forEach(identity => stanza.c('identity', {'category': identity.category, 'type': identity.type}).up());
     features?.forEach(feature => stanza.c('feature', {'var': feature}).up());
     items?.forEach(item => stanza.c('item', {'jid': item}).up());
-    _converse.connection._dataRecv(mock.createRequest(stanza));
+    _converse.connection._dataRecv(createRequest(stanza));
 }
 
-mock.createRequest = function (iq) {
+function createRequest (iq) {
     iq = typeof iq.tree == "function" ? iq.tree() : iq;
     var req = new Strophe.Request(iq, function() {});
     req.getResponse = function () {
@@ -61,13 +59,13 @@ mock.createRequest = function (iq) {
         return env;
     };
     return req;
-};
+}
 
-mock.closeAllChatBoxes = function (_converse) {
+function closeAllChatBoxes (_converse) {
     return Promise.all(_converse.chatboxviews.map(view => view.close()));
-};
+}
 
-mock.toggleControlBox = function () {
+function toggleControlBox () {
     const toggle = document.querySelector(".toggle-controlbox");
     if (!u.isVisible(document.querySelector("#controlbox"))) {
         if (!u.isVisible(toggle)) {
@@ -77,24 +75,24 @@ mock.toggleControlBox = function () {
     }
 }
 
-mock.openControlBox = async function (_converse) {
+async function openControlBox(_converse) {
     const model = await _converse.api.controlbox.open();
     await u.waitUntil(() => model.get('connected'));
-    mock.toggleControlBox();
+    toggleControlBox();
     return this;
-};
+}
 
-mock.closeControlBox = function () {
+function closeControlBox () {
     const controlbox = document.querySelector("#controlbox");
     if (u.isVisible(controlbox)) {
         const button = controlbox.querySelector(".close-chatbox-button");
         (button !== null) && button.click();
     }
     return this;
-};
+}
 
-mock.waitUntilBookmarksReturned = async function (_converse, bookmarks=[]) {
-    await mock.waitUntilDiscoConfirmed(
+async function waitUntilBookmarksReturned (_converse, bookmarks=[]) {
+    await waitUntilDiscoConfirmed(
         _converse, _converse.bare_jid,
         [{'category': 'pubsub', 'type': 'pep'}],
         ['http://jabber.org/protocol/pubsub#publish-options']
@@ -118,30 +116,30 @@ mock.waitUntilBookmarksReturned = async function (_converse, bookmarks=[]) {
             'jid': bookmark.jid
         }).c('nick').t(bookmark.nick).up().up()
     });
-    _converse.connection._dataRecv(mock.createRequest(stanza));
+    _converse.connection._dataRecv(createRequest(stanza));
     await _converse.api.waitUntil('bookmarksInitialized');
-};
+}
 
-mock.openChatBoxes = function (converse, amount) {
+function openChatBoxes (converse, amount) {
     for (let i=0; i<amount; i++) {
-        const jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+        const jid = cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit';
         converse.roster.get(jid).openChat();
     }
-};
+}
 
-mock.openChatBoxFor = async function (_converse, jid) {
+async function openChatBoxFor (_converse, jid) {
     await _converse.api.waitUntil('rosterContactsFetched');
     _converse.roster.get(jid).openChat();
     return u.waitUntil(() => _converse.chatboxviews.get(jid), 1000);
-};
+}
 
-mock.openChatRoomViaModal = async function (_converse, jid, nick='') {
+async function openChatRoomViaModal (_converse, jid, nick='') {
     // Opens a new chatroom
     const model = await _converse.api.controlbox.open('controlbox');
     await u.waitUntil(() => model.get('connected'));
-    await mock.openControlBox(_converse);
+    await openControlBox(_converse);
     document.querySelector('converse-rooms-list .show-add-muc-modal').click();
-    mock.closeControlBox(_converse);
+    closeControlBox(_converse);
     const modal = _converse.api.modal.get('add-chatroom-modal');
     await u.waitUntil(() => u.isVisible(modal.el), 1500)
     modal.el.querySelector('input[name="chatroom"]').value = jid;
@@ -151,13 +149,13 @@ mock.openChatRoomViaModal = async function (_converse, jid, nick='') {
     modal.el.querySelector('form input[type="submit"]').click();
     await u.waitUntil(() => _converse.chatboxviews.get(jid), 1000);
     return _converse.chatboxviews.get(jid);
-};
+}
 
-mock.openChatRoom = function (_converse, room, server) {
+function openChatRoom (_converse, room, server) {
     return _converse.api.rooms.open(`${room}@${server}`);
-};
+}
 
-mock.getRoomFeatures = async function (_converse, muc_jid, features=[]) {
+async function getRoomFeatures (_converse, muc_jid, features=[]) {
     const room = Strophe.getNodeFromJid(muc_jid);
     muc_jid = muc_jid.toLowerCase();
     const stanzas = _converse.connection.IQ_stanzas;
@@ -178,7 +176,7 @@ mock.getRoomFeatures = async function (_converse, muc_jid, features=[]) {
             'type': 'text'
         }).up();
 
-    features = features.length ? features : mock.default_muc_features;
+    features = features.length ? features : default_muc_features;
     features.forEach(f => features_stanza.c('feature', {'var': f}).up());
     features_stanza.c('x', { 'xmlns':'jabber:x:data', 'type':'result'})
         .c('field', {'var':'FORM_TYPE', 'type':'hidden'})
@@ -187,11 +185,11 @@ mock.getRoomFeatures = async function (_converse, muc_jid, features=[]) {
             .c('value').t('This is the description').up().up()
         .c('field', {'type':'text-single', 'var':'muc#roominfo_occupants', 'label':'Number of occupants'})
             .c('value').t(0);
-    _converse.connection._dataRecv(mock.createRequest(features_stanza));
-};
+    _converse.connection._dataRecv(createRequest(features_stanza));
+}
 
 
-mock.waitForReservedNick = async function (_converse, muc_jid, nick) {
+async function waitForReservedNick (_converse, muc_jid, nick) {
     const stanzas = _converse.connection.IQ_stanzas;
     const selector = `iq[to="${muc_jid.toLowerCase()}"] query[node="x-roomuser-item"]`;
     const iq = await u.waitUntil(() => stanzas.filter(s => sizzle(selector, s).length).pop());
@@ -210,14 +208,14 @@ mock.waitForReservedNick = async function (_converse, muc_jid, nick) {
     if (nick) {
         stanza.c('identity', {'category': 'conference', 'name': nick, 'type': 'text'});
     }
-    _converse.connection._dataRecv(mock.createRequest(stanza));
+    _converse.connection._dataRecv(createRequest(stanza));
     if (nick) {
         return u.waitUntil(() => nick);
     }
-};
+}
 
 
-mock.returnMemberLists = async function (_converse, muc_jid, members=[], affiliations=['member', 'owner', 'admin']) {
+async function returnMemberLists (_converse, muc_jid, members=[], affiliations=['member', 'owner', 'admin']) {
     if (affiliations.length === 0) {
         return;
     }
@@ -240,7 +238,7 @@ mock.returnMemberLists = async function (_converse, muc_jid, members=[], affilia
                 'nick': m.nick
             });
         });
-        _converse.connection._dataRecv(mock.createRequest(member_list_stanza));
+        _converse.connection._dataRecv(createRequest(member_list_stanza));
     }
 
     if (affiliations.includes('admin')) {
@@ -260,7 +258,7 @@ mock.returnMemberLists = async function (_converse, muc_jid, members=[], affilia
                 'nick': m.nick
             });
         });
-        _converse.connection._dataRecv(mock.createRequest(admin_list_stanza));
+        _converse.connection._dataRecv(createRequest(admin_list_stanza));
     }
 
     if (affiliations.includes('owner')) {
@@ -280,12 +278,12 @@ mock.returnMemberLists = async function (_converse, muc_jid, members=[], affilia
                 'nick': m.nick
             });
         });
-        _converse.connection._dataRecv(mock.createRequest(owner_list_stanza));
+        _converse.connection._dataRecv(createRequest(owner_list_stanza));
     }
     return new Promise(resolve => _converse.api.listen.on('membersFetched', resolve));
-};
+}
 
-mock.receiveOwnMUCPresence = async function (_converse, muc_jid, nick) {
+async function receiveOwnMUCPresence (_converse, muc_jid, nick) {
     const sent_stanzas = _converse.connection.sent_stanzas;
     await u.waitUntil(() => sent_stanzas.filter(iq => sizzle('presence history', iq).length).pop());
     const presence = $pres({
@@ -299,20 +297,20 @@ mock.receiveOwnMUCPresence = async function (_converse, muc_jid, nick) {
             role: 'moderator'
         }).up()
         .c('status').attrs({code:'110'});
-    _converse.connection._dataRecv(mock.createRequest(presence));
-};
+    _converse.connection._dataRecv(createRequest(presence));
+}
 
 
-mock.openAndEnterChatRoom = async function (_converse, muc_jid, nick, features=[], members=[], force_open=true, settings={}) {
+async function openAndEnterChatRoom (_converse, muc_jid, nick, features=[], members=[], force_open=true, settings={}) {
     const { api } = _converse;
     muc_jid = muc_jid.toLowerCase();
     const room_creation_promise = api.rooms.open(muc_jid, settings, force_open);
-    await mock.getRoomFeatures(_converse, muc_jid, features);
-    await mock.waitForReservedNick(_converse, muc_jid, nick);
+    await getRoomFeatures(_converse, muc_jid, features);
+    await waitForReservedNick(_converse, muc_jid, nick);
     // The user has just entered the room (because join was called)
     // and receives their own presence from the server.
     // See example 24: https://xmpp.org/extensions/xep-0045.html#enter-pres
-    await mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
+    await receiveOwnMUCPresence(_converse, muc_jid, nick);
 
     await room_creation_promise;
     const model = _converse.chatboxes.get(muc_jid);
@@ -320,12 +318,12 @@ mock.openAndEnterChatRoom = async function (_converse, muc_jid, nick, features=[
 
     const affs = _converse.muc_fetch_members;
     const all_affiliations = Array.isArray(affs) ? affs :  (affs ? ['member', 'admin', 'owner'] : []);
-    await mock.returnMemberLists(_converse, muc_jid, members, all_affiliations);
+    await returnMemberLists(_converse, muc_jid, members, all_affiliations);
     await model.messages.fetched;
     return model;
-};
+}
 
-mock.createContact = async function (_converse, name, ask, requesting, subscription) {
+async function createContact (_converse, name, ask, requesting, subscription) {
     const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
     if (_converse.roster.get(jid)) {
         return Promise.resolve();
@@ -340,9 +338,9 @@ mock.createContact = async function (_converse, name, ask, requesting, subscript
         }, {success, error});
     });
     return contact;
-};
+}
 
-mock.createContacts = async function (_converse, type, length) {
+async function createContacts (_converse, type, length) {
     /* Create current (as opposed to requesting or pending) contacts
         * for the user's roster.
         *
@@ -351,17 +349,17 @@ mock.createContacts = async function (_converse, type, length) {
     await _converse.api.waitUntil('rosterContactsFetched');
     let names, subscription, requesting, ask;
     if (type === 'requesting') {
-        names = mock.req_names;
+        names = req_names;
         subscription = 'none';
         requesting = true;
         ask = null;
     } else if (type === 'pending') {
-        names = mock.pend_names;
+        names = pend_names;
         subscription = 'none';
         requesting = false;
         ask = 'subscribe';
     } else if (type === 'current') {
-        names = mock.cur_names;
+        names = cur_names;
         subscription = 'both';
         requesting = false;
         ask = null;
@@ -375,9 +373,9 @@ mock.createContacts = async function (_converse, type, length) {
     }
     const promises = names.slice(0, length).map(n => this.createContact(_converse, n, ask, requesting, subscription));
     await Promise.all(promises);
-};
+}
 
-mock.waitForRoster = async function (_converse, type='current', length=-1, include_nick=true, grouped=true) {
+async function waitForRoster (_converse, type='current', length=-1, include_nick=true, grouped=true) {
     const s = `iq[type="get"] query[xmlns="${Strophe.NS.ROSTER}"]`;
     const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(iq => sizzle(s, iq).length).pop());
 
@@ -389,8 +387,7 @@ mock.waitForRoster = async function (_converse, type='current', length=-1, inclu
         'xmlns': 'jabber:iq:roster'
     });
     if (type === 'pending' || type === 'all') {
-        const pend_names = (length > -1) ? mock.pend_names.slice(0, length) : mock.pend_names;
-        pend_names.map(name =>
+        ((length > -1) ? pend_names.slice(0, length) : pend_names).map(name =>
             result.c('item', {
                 jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit',
                 name: include_nick ? name : undefined,
@@ -400,7 +397,7 @@ mock.waitForRoster = async function (_converse, type='current', length=-1, inclu
         );
     }
     if (type === 'current' || type === 'all') {
-        const cur_names = Object.keys(mock.current_contacts_map);
+        const cur_names = Object.keys(current_contacts_map);
         const names = (length > -1) ? cur_names.slice(0, length) : cur_names;
         names.forEach(name => {
             result.c('item', {
@@ -410,16 +407,16 @@ mock.waitForRoster = async function (_converse, type='current', length=-1, inclu
                 ask: null
             });
             if (grouped) {
-                mock.current_contacts_map[name].forEach(g => result.c('group').t(g).up());
+                current_contacts_map[name].forEach(g => result.c('group').t(g).up());
             }
             result.up();
         });
     }
-    _converse.connection._dataRecv(mock.createRequest(result));
+    _converse.connection._dataRecv(createRequest(result));
     await _converse.api.waitUntil('rosterContactsFetched');
-};
+}
 
-mock.createChatMessage = function (_converse, sender_jid, message) {
+function createChatMessage (_converse, sender_jid, message) {
     return $msg({
                 from: sender_jid,
                 to: _converse.connection.jid,
@@ -431,7 +428,7 @@ mock.createChatMessage = function (_converse, sender_jid, message) {
             .c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree();
 }
 
-mock.sendMessage = async function (view, message) {
+async function sendMessage (view, message) {
     const promise = new Promise(resolve => view.model.messages.once('rendered', resolve));
     const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
     textarea.value = message;
@@ -442,8 +439,7 @@ mock.sendMessage = async function (view, message) {
         keyCode: 13
     });
     return promise;
-};
-
+}
 
 window.libsignal = {
     'SignalProtocolAddress': function (name, device_id) {
@@ -500,9 +496,9 @@ window.libsignal = {
             });
         }
     }
-};
+}
 
-mock.default_muc_features = [
+const default_muc_features = [
     'http://jabber.org/protocol/muc',
     'jabber:iq:register',
     Strophe.NS.SID,
@@ -515,16 +511,16 @@ mock.default_muc_features = [
     'muc_anonymous'
 ];
 
-mock.view_mode = 'overlayed';
+const view_mode = 'overlayed';
 
 // Names from http://www.fakenamegenerator.com/
-mock.req_names = [
+const req_names = [
     'Escalus, prince of Verona', 'The Nurse', 'Paris'
 ];
-mock.pend_names = [
+const pend_names = [
     'Lord Capulet', 'Guard', 'Servant'
 ];
-mock.current_contacts_map = {
+const current_contacts_map = {
     'Mercutio': ['Colleagues', 'friends & acquaintences'],
     'Juliet Capulet': ['friends & acquaintences'],
     'Lady Montague': ['Colleagues', 'Family'],
@@ -540,9 +536,9 @@ mock.current_contacts_map = {
     'Gregory': ['friends & acquaintences'],
     'Potpan': [],
     'Friar John': []
-};
+}
 
-const map = mock.current_contacts_map;
+const map = current_contacts_map;
 const groups_map = {};
 Object.keys(map).forEach(k => {
     const groups = map[k].length ? map[k] : ["Ungrouped"];
@@ -550,20 +546,19 @@ Object.keys(map).forEach(k => {
         groups_map[g] = groups_map[g] ? [...groups_map[g], k] : [k]
     });
 });
-mock.groups_map = groups_map;
 
-mock.cur_names = Object.keys(mock.current_contacts_map);
-mock.num_contacts = mock.req_names.length + mock.pend_names.length + mock.cur_names.length;
+const cur_names = Object.keys(current_contacts_map);
+const num_contacts = req_names.length + pend_names.length + cur_names.length;
 
-mock.groups = {
+const groups = {
     'colleagues': 3,
     'friends & acquaintences': 3,
     'Family': 4,
     'ænemies': 3,
     'Ungrouped': 2
-};
+}
 
-mock.chatroom_names = [
+const chatroom_names = [
     'Dyon van de Wege',
     'Thomas Kalb',
     'Dirk Theissen',
@@ -571,19 +566,20 @@ mock.chatroom_names = [
     'Ka Lek',
     'Anne Ebersbacher'
 ];
+
 // TODO: need to also test other roles and affiliations
-mock.chatroom_roles = {
+const chatroom_roles = {
     'Anne Ebersbacher': { affiliation: "owner", role: "moderator" },
     'Dirk Theissen': { affiliation: "admin", role: "moderator" },
     'Dyon van de Wege': { affiliation: "member", role: "occupant" },
     'Felix Hofmann': { affiliation: "member", role: "occupant" },
     'Ka Lek': { affiliation: "member", role: "occupant" },
     'Thomas Kalb': { affiliation: "member", role: "occupant" }
-};
+}
 
-mock.event = {
+const event = {
     'preventDefault': function () {}
-};
+}
 
 function clearIndexedDB () {
     const promise = u.getOpenPromise();
@@ -615,7 +611,7 @@ function clearStores () {
     window.sessionStorage.removeItem(cache_key+'fetched');
 }
 
-const initConverse = async (settings) => {
+async function _initConverse (settings) {
     clearStores();
     await clearIndexedDB();
 
@@ -631,7 +627,7 @@ const initConverse = async (settings) => {
         'no_trimming': true,
         'play_sounds': false,
         'use_emojione': false,
-        'view_mode': mock.view_mode
+        'view_mode': view_mode
     }, settings || {}));
 
     window._converse = _converse;
@@ -674,7 +670,7 @@ const initConverse = async (settings) => {
 }
 
 
-mock.deviceListFetched = async function deviceListFetched (_converse, jid) {
+async function deviceListFetched (_converse, jid) {
     const selector = `iq[to="${jid}"] items[node="eu.siacs.conversations.axolotl.devicelist"]`;
     const stanza = await u.waitUntil(
         () => Array.from(_converse.connection.IQ_stanzas).filter(iq => iq.querySelector(selector)).pop()
@@ -683,30 +679,30 @@ mock.deviceListFetched = async function deviceListFetched (_converse, jid) {
     return stanza;
 }
 
-mock.ownDeviceHasBeenPublished = function ownDeviceHasBeenPublished (_converse) {
+function ownDeviceHasBeenPublished (_converse) {
     return Array.from(_converse.connection.IQ_stanzas).filter(
         iq => iq.querySelector('iq[from="'+_converse.bare_jid+'"] publish[node="eu.siacs.conversations.axolotl.devicelist"]')
     ).pop();
 }
 
-mock.bundleHasBeenPublished = function bundleHasBeenPublished (_converse) {
+function bundleHasBeenPublished (_converse) {
     const selector = 'publish[node="eu.siacs.conversations.axolotl.bundles:123456789"]';
     return Array.from(_converse.connection.IQ_stanzas).filter(iq => iq.querySelector(selector)).pop();
 }
 
-mock.bundleFetched = function bundleFetched (_converse, jid, device_id) {
+function bundleFetched (_converse, jid, device_id) {
     return Array.from(_converse.connection.IQ_stanzas).filter(
         iq => iq.querySelector(`iq[to="${jid}"] items[node="eu.siacs.conversations.axolotl.bundles:${device_id}"]`)
     ).pop();
 }
 
-mock.initializedOMEMO = async function initializedOMEMO (_converse) {
-    await mock.waitUntilDiscoConfirmed(
+async function initializedOMEMO (_converse) {
+    await waitUntilDiscoConfirmed(
         _converse, _converse.bare_jid,
         [{'category': 'pubsub', 'type': 'pep'}],
         ['http://jabber.org/protocol/pubsub#publish-options']
     );
-    let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, _converse.bare_jid));
+    let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid));
     let stanza = $iq({
         'from': _converse.bare_jid,
         'id': iq_stanza.getAttribute('id'),
@@ -717,23 +713,67 @@ mock.initializedOMEMO = async function initializedOMEMO (_converse) {
             .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
                 .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
                     .c('device', {'id': '482886413b977930064a5888b92134fe'});
-    _converse.connection._dataRecv(mock.createRequest(stanza));
-    iq_stanza = await u.waitUntil(() => mock.ownDeviceHasBeenPublished(_converse))
+    _converse.connection._dataRecv(createRequest(stanza));
+    iq_stanza = await u.waitUntil(() => ownDeviceHasBeenPublished(_converse))
 
     stanza = $iq({
         'from': _converse.bare_jid,
         'id': iq_stanza.getAttribute('id'),
         'to': _converse.bare_jid,
         'type': 'result'});
-    _converse.connection._dataRecv(mock.createRequest(stanza));
+    _converse.connection._dataRecv(createRequest(stanza));
 
-    iq_stanza = await u.waitUntil(() => mock.bundleHasBeenPublished(_converse))
+    iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse))
 
     stanza = $iq({
         'from': _converse.bare_jid,
         'id': iq_stanza.getAttribute('id'),
         'to': _converse.bare_jid,
         'type': 'result'});
-    _converse.connection._dataRecv(mock.createRequest(stanza));
+    _converse.connection._dataRecv(createRequest(stanza));
     await _converse.api.waitUntil('OMEMOInitialized');
 }
+
+Object.assign(mock, {
+    bundleFetched,
+    bundleHasBeenPublished,
+    chatroom_names,
+    chatroom_roles,
+    closeAllChatBoxes,
+    closeControlBox,
+    createChatMessage,
+    createContact,
+    createContacts,
+    createRequest,
+    cur_names,
+    current_contacts_map,
+    default_muc_features,
+    deviceListFetched,
+    event,
+    getRoomFeatures,
+    groups,
+    groups_map,
+    initConverse,
+    initializedOMEMO,
+    num_contacts,
+    openAndEnterChatRoom,
+    openChatBoxFor,
+    openChatBoxes,
+    openChatRoom,
+    openChatRoomViaModal,
+    openControlBox,
+    ownDeviceHasBeenPublished,
+    pend_names,
+    receiveOwnMUCPresence,
+    req_names,
+    returnMemberLists,
+    sendMessage,
+    toggleControlBox,
+    view_mode,
+    waitForReservedNick,
+    waitForRoster,
+    waitUntilBookmarksReturned,
+    waitUntilDiscoConfirmed
+});
+
+window.mock = mock;