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

Merge pull request #1269 from conversejs/async-await

Async await
JC Brand 6 жил өмнө
parent
commit
37f4398974

+ 6 - 3
css/converse.css

@@ -9569,12 +9569,15 @@ body.reset {
   max-height: 200px; }
 #conversejs.converse-overlayed .emoji-picker {
   height: 100px; }
-#conversejs.converse-overlayed .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu {
+#conversejs.converse-overlayed .chatbox .sendXMPPMessage .chat-toolbar li .toolbar-menu {
   min-width: 235px; }
-  #conversejs.converse-overlayed .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul.emoji-toolbar {
+  #conversejs.converse-overlayed .chatbox .sendXMPPMessage .chat-toolbar li .toolbar-menu ul.emoji-toolbar {
     width: 100%; }
-    #conversejs.converse-overlayed .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul.emoji-toolbar .emoji-category {
+    #conversejs.converse-overlayed .chatbox .sendXMPPMessage .chat-toolbar li .toolbar-menu ul.emoji-toolbar .emoji-category {
       float: left; }
+#conversejs.converse-overlayed .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker ul {
+  flex-wrap: wrap;
+  justify-content: flex-start; }
 
 @media (max-width: 767.98px) {
   #conversejs.converse-overlayed > .row {

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 248 - 230
dist/converse.js


+ 11 - 1
sass/_chatbox.scss

@@ -527,7 +527,7 @@
         height: $overlayed-emoji-picker-height;
     }
     .chatbox {
-        form.sendXMPPMessage {
+        .sendXMPPMessage {
             .chat-toolbar {
                 li {
                     .toolbar-menu {
@@ -541,6 +541,16 @@
                             }
                         }
                     }
+                    &.toggle-smiley {
+                        .emoji-toolbar {
+                            .emoji-category-picker {
+                                ul {
+                                    flex-wrap: wrap;
+                                    justify-content: flex-start;
+                                }
+                            }
+                        }
+                    }
                 }
             }
         }

+ 214 - 209
spec/bookmarks.js

@@ -9,12 +9,13 @@
         ], factory);
 } (this, function (jasmine, $, mock, test_utils) {
     "use strict";
-    var $iq = converse.env.$iq,
-        $msg = converse.env.$msg,
-        Backbone = converse.env.Backbone,
-        Strophe = converse.env.Strophe,
-        _ = converse.env._,
-        u = converse.env.utils;
+    const $iq = converse.env.$iq,
+         $msg = converse.env.$msg,
+         Backbone = converse.env.Backbone,
+         Strophe = converse.env.Strophe,
+         sizzle = converse.env.sizzle,
+         _ = converse.env._,
+         u = converse.env.utils;
 
     describe("A chat room", function () {
 
@@ -369,242 +370,246 @@
             });
         }));
 
+
         it("can be retrieved from the XMPP server", mock.initConverseWithPromises(
             ['send'], ['chatBoxesFetched', 'roomsPanelRendered', 'rosterGroupsFetched'], {},
-            function (done, _converse) {
+            async function (done, _converse) {
 
-            test_utils.waitUntilDiscoConfirmed(
+            await test_utils.waitUntilDiscoConfirmed(
                 _converse, _converse.bare_jid,
                 [{'category': 'pubsub', 'type': 'pep'}],
                 ['http://jabber.org/protocol/pubsub#publish-options']
-            ).then(function () {
-                /* Client requests all items
-                 * -------------------------
-                 *
-                 *  <iq from='juliet@capulet.lit/randomID' type='get' id='retrieve1'>
-                 *  <pubsub xmlns='http://jabber.org/protocol/pubsub'>
-                 *      <items node='storage:bookmarks'/>
-                 *  </pubsub>
-                 *  </iq>
-                 */
-                var IQ_id;
-                expect(_.filter(_converse.connection.send.calls.all(), function (call) {
-                    var stanza = call.args[0];
-                    if (!(stanza instanceof Element) || stanza.nodeName !== 'iq') {
-                        return;
-                    }
-                    // XXX: Wrapping in a div is a workaround for PhantomJS
-                    var div = document.createElement('div');
-                    div.appendChild(stanza);
-                    if (div.innerHTML ===
-                        '<iq from="dummy@localhost/resource" type="get" '+
-                            'xmlns="jabber:client" id="'+stanza.getAttribute('id')+'">'+
-                        '<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
-                            '<items node="storage:bookmarks"></items>'+
-                        '</pubsub>'+
-                        '</iq>') {
-                        IQ_id = stanza.getAttribute('id');
-                        return true;
+            );
+            /* Client requests all items
+             * -------------------------
+             *
+             *  <iq from='juliet@capulet.lit/randomID' type='get' id='retrieve1'>
+             *  <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+             *      <items node='storage:bookmarks'/>
+             *  </pubsub>
+             *  </iq>
+             */
+            let IQ_id;
+            const call = await test_utils.waitUntil(() =>
+                _.filter(
+                    _converse.connection.send.calls.all(),
+                    call => {
+                        const stanza = call.args[0];
+                        if (!(stanza instanceof Element) || stanza.nodeName !== 'iq') {
+                            return;
+                        }
+                        if (sizzle('items[node="storage:bookmarks"]', stanza).length) {
+                            IQ_id = stanza.getAttribute('id');
+                            return true;
+                        }
                     }
-                }).length).toBe(1);
-
-                /*
-                 * Server returns all items
-                 * ------------------------
-                 * <iq type='result'
-                 *     to='juliet@capulet.lit/randomID'
-                 *     id='retrieve1'>
-                 * <pubsub xmlns='http://jabber.org/protocol/pubsub'>
-                 *     <items node='storage:bookmarks'>
-                 *     <item id='current'>
-                 *         <storage xmlns='storage:bookmarks'>
-                 *         <conference name='The Play&apos;s the Thing'
-                 *                     autojoin='true'
-                 *                     jid='theplay@conference.shakespeare.lit'>
-                 *             <nick>JC</nick>
-                 *         </conference>
-                 *         </storage>
-                 *     </item>
-                 *     </items>
-                 * </pubsub>
-                 * </iq>
-                 */
-                expect(_converse.bookmarks.models.length).toBe(0);
-
-                spyOn(_converse.bookmarks, 'onBookmarksReceived').and.callThrough();
-                var stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':IQ_id})
+                ).pop()
+            );
+
+            expect(Strophe.serialize(call.args[0])).toBe(
+                `<iq from="dummy@localhost/resource" id="${IQ_id}" type="get" xmlns="jabber:client">`+
+                '<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
+                    '<items node="storage:bookmarks"/>'+
+                '</pubsub>'+
+                '</iq>');
+                
+            /*
+             * Server returns all items
+             * ------------------------
+             * <iq type='result'
+             *     to='juliet@capulet.lit/randomID'
+             *     id='retrieve1'>
+             * <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+             *     <items node='storage:bookmarks'>
+             *     <item id='current'>
+             *         <storage xmlns='storage:bookmarks'>
+             *         <conference name='The Play&apos;s the Thing'
+             *                     autojoin='true'
+             *                     jid='theplay@conference.shakespeare.lit'>
+             *             <nick>JC</nick>
+             *         </conference>
+             *         </storage>
+             *     </item>
+             *     </items>
+             * </pubsub>
+             * </iq>
+             */
+            expect(_converse.bookmarks.models.length).toBe(0);
+
+            spyOn(_converse.bookmarks, 'onBookmarksReceived').and.callThrough();
+            var stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':IQ_id})
+                .c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
+                    .c('items', {'node': 'storage:bookmarks'})
+                        .c('item', {'id': 'current'})
+                            .c('storage', {'xmlns': 'storage:bookmarks'})
+                                .c('conference', {
+                                    'name': 'The Play&apos;s the Thing',
+                                    'autojoin': 'true',
+                                    'jid': 'theplay@conference.shakespeare.lit'
+                                }).c('nick').t('JC').up().up()
+                                .c('conference', {
+                                    'name': 'Another room',
+                                    'autojoin': 'false',
+                                    'jid': 'another@conference.shakespeare.lit'
+                                }); // Purposefully exclude the <nick> element to test #1043
+            _converse.connection._dataRecv(test_utils.createRequest(stanza));
+            await test_utils.waitUntil(() => _converse.bookmarks.onBookmarksReceived.calls.count());
+            expect(_converse.bookmarks.models.length).toBe(2);
+            expect(_converse.bookmarks.findWhere({'jid': 'theplay@conference.shakespeare.lit'}).get('autojoin')).toBe(true);
+            expect(_converse.bookmarks.findWhere({'jid': 'another@conference.shakespeare.lit'}).get('autojoin')).toBe(false);
+            done();
+        }));
+
+        describe("The rooms panel", function () {
+
+            it("shows a list of bookmarks", mock.initConverseWithPromises(
+                ['send'], ['rosterGroupsFetched'], {},
+                async function (done, _converse) {
+
+                await test_utils.waitUntilDiscoConfirmed(
+                    _converse, _converse.bare_jid,
+                    [{'category': 'pubsub', 'type': 'pep'}],
+                    ['http://jabber.org/protocol/pubsub#publish-options']
+                );
+                test_utils.openControlBox();
+
+                let IQ_id;
+                const call = await test_utils.waitUntil(() =>
+                    _.filter(
+                        _converse.connection.send.calls.all(),
+                        call => {
+                            const stanza = call.args[0];
+                            if (!(stanza instanceof Element) || stanza.nodeName !== 'iq') {
+                                return;
+                            }
+                            if (sizzle('items[node="storage:bookmarks"]', stanza).length) {
+                                IQ_id = stanza.getAttribute('id');
+                                return true;
+                            }
+                        }
+                    ).pop()
+                );
+                expect(Strophe.serialize(call.args[0])).toBe(
+                    `<iq from="dummy@localhost/resource" id="${IQ_id}" type="get" xmlns="jabber:client">`+
+                    '<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
+                        '<items node="storage:bookmarks"/>'+
+                    '</pubsub>'+
+                    '</iq>'
+                );
+
+                const stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':IQ_id})
                     .c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
                         .c('items', {'node': 'storage:bookmarks'})
                             .c('item', {'id': 'current'})
                                 .c('storage', {'xmlns': 'storage:bookmarks'})
                                     .c('conference', {
                                         'name': 'The Play&apos;s the Thing',
-                                        'autojoin': 'true',
+                                        'autojoin': 'false',
                                         'jid': 'theplay@conference.shakespeare.lit'
                                     }).c('nick').t('JC').up().up()
+                                    .c('conference', {
+                                        'name': '1st Bookmark',
+                                        'autojoin': 'false',
+                                        'jid': 'first@conference.shakespeare.lit'
+                                    }).c('nick').t('JC').up().up()
+                                    .c('conference', {
+                                        'autojoin': 'false',
+                                        'jid': 'noname@conference.shakespeare.lit'
+                                    }).c('nick').t('JC').up().up()
+                                    .c('conference', {
+                                        'name': 'Bookmark with a very very long name that will be shortened',
+                                        'autojoin': 'false',
+                                        'jid': 'longname@conference.shakespeare.lit'
+                                    }).c('nick').t('JC').up().up()
                                     .c('conference', {
                                         'name': 'Another room',
                                         'autojoin': 'false',
                                         'jid': 'another@conference.shakespeare.lit'
-                                    }); // Purposefully exclude the <nick> element to test #1043
+                                    }).c('nick').t('JC').up().up();
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
-                return test_utils.waitUntil(() => _converse.bookmarks.onBookmarksReceived.calls.count(), 300)
-            }).then(() => {
-                expect(_converse.bookmarks.models.length).toBe(2);
-                expect(_converse.bookmarks.findWhere({'jid': 'theplay@conference.shakespeare.lit'}).get('autojoin')).toBe(true);
-                expect(_converse.bookmarks.findWhere({'jid': 'another@conference.shakespeare.lit'}).get('autojoin')).toBe(false);
-                done();
-            }).catch(_.partial(console.error, _));
-        }));
-
-        describe("The rooms panel", function () {
-
-            it("shows a list of bookmarks", mock.initConverseWithPromises(
-                ['send'], ['rosterGroupsFetched'], {}, function (done, _converse) {
 
-                test_utils.waitUntilDiscoConfirmed(
-                    _converse, _converse.bare_jid,
-                    [{'category': 'pubsub', 'type': 'pep'}],
-                    ['http://jabber.org/protocol/pubsub#publish-options']
-                ).then(function () {
-                    test_utils.openControlBox();
-
-                    var IQ_id;
-                    expect(_.filter(_converse.connection.send.calls.all(), function (call) {
-                        var stanza = call.args[0];
-                        if (!(stanza instanceof Element) || stanza.nodeName !== 'iq') {
-                            return;
-                        }
-                        // XXX: Wrapping in a div is a workaround for PhantomJS
-                        var div = document.createElement('div');
-                        div.appendChild(stanza);
-                        if (div.innerHTML ===
-                            '<iq from="dummy@localhost/resource" type="get" '+
-                                'xmlns="jabber:client" id="'+stanza.getAttribute('id')+'">'+
-                            '<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
-                                '<items node="storage:bookmarks"></items>'+
-                            '</pubsub>'+
-                            '</iq>') {
-                            IQ_id = stanza.getAttribute('id');
-                            return true;
-                        }
-                    }).length).toBe(1);
-
-                    var stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':IQ_id})
-                        .c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
-                            .c('items', {'node': 'storage:bookmarks'})
-                                .c('item', {'id': 'current'})
-                                    .c('storage', {'xmlns': 'storage:bookmarks'})
-                                        .c('conference', {
-                                            'name': 'The Play&apos;s the Thing',
-                                            'autojoin': 'false',
-                                            'jid': 'theplay@conference.shakespeare.lit'
-                                        }).c('nick').t('JC').up().up()
-                                        .c('conference', {
-                                            'name': '1st Bookmark',
-                                            'autojoin': 'false',
-                                            'jid': 'first@conference.shakespeare.lit'
-                                        }).c('nick').t('JC').up().up()
-                                        .c('conference', {
-                                            'autojoin': 'false',
-                                            'jid': 'noname@conference.shakespeare.lit'
-                                        }).c('nick').t('JC').up().up()
-                                        .c('conference', {
-                                            'name': 'Bookmark with a very very long name that will be shortened',
-                                            'autojoin': 'false',
-                                            'jid': 'longname@conference.shakespeare.lit'
-                                        }).c('nick').t('JC').up().up()
-                                        .c('conference', {
-                                            'name': 'Another room',
-                                            'autojoin': 'false',
-                                            'jid': 'another@conference.shakespeare.lit'
-                                        }).c('nick').t('JC').up().up();
-                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
-
-                    test_utils.waitUntil(() => document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item').length, 300)
-                    .then(() => {
-                        expect(document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item').length).toBe(5);
-                        const els = document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item a.list-item-link');
-                        expect(els[0].textContent).toBe("1st Bookmark");
-                        expect(els[1].textContent).toBe("Another room");
-                        expect(els[2].textContent).toBe("Bookmark with a very very long name that will be shortened");
-                        expect(els[3].textContent).toBe("noname@conference.shakespeare.lit");
-                        expect(els[4].textContent).toBe("The Play's the Thing");
-
-                        spyOn(window, 'confirm').and.returnValue(true);
-                        document.querySelector('#chatrooms .bookmarks.rooms-list .room-item:nth-child(2) a:nth-child(2)').click();
-                        expect(window.confirm).toHaveBeenCalled();
-                        return test_utils.waitUntil(() => document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item').length === 4, 300)
-                    }).then(() => {
-                        const els = document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item a.list-item-link');
-                        expect(els[0].textContent).toBe("1st Bookmark");
-                        expect(els[1].textContent).toBe("Bookmark with a very very long name that will be shortened");
-                        expect(els[2].textContent).toBe("noname@conference.shakespeare.lit");
-                        expect(els[3].textContent).toBe("The Play's the Thing");
-                        done();
-                    }).catch(_.partial(console.error, _));
-                });
+                await test_utils.waitUntil(() => document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item').length);
+                expect(document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item').length).toBe(5);
+                let els = document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item a.list-item-link');
+                expect(els[0].textContent).toBe("1st Bookmark");
+                expect(els[1].textContent).toBe("Another room");
+                expect(els[2].textContent).toBe("Bookmark with a very very long name that will be shortened");
+                expect(els[3].textContent).toBe("noname@conference.shakespeare.lit");
+                expect(els[4].textContent).toBe("The Play's the Thing");
+
+                spyOn(window, 'confirm').and.returnValue(true);
+                document.querySelector('#chatrooms .bookmarks.rooms-list .room-item:nth-child(2) a:nth-child(2)').click();
+                expect(window.confirm).toHaveBeenCalled();
+                await test_utils.waitUntil(() => document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item').length === 4)
+                els = document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item a.list-item-link');
+                expect(els[0].textContent).toBe("1st Bookmark");
+                expect(els[1].textContent).toBe("Bookmark with a very very long name that will be shortened");
+                expect(els[2].textContent).toBe("noname@conference.shakespeare.lit");
+                expect(els[3].textContent).toBe("The Play's the Thing");
+                done();
             }));
 
+
             it("remembers the toggle state of the bookmarks list", mock.initConverseWithPromises(
-                ['send'], ['rosterGroupsFetched'], {}, function (done, _converse) {
+                ['send'], ['rosterGroupsFetched'], {},
+                async function (done, _converse) {
 
                 test_utils.openControlBox();
-
-                test_utils.waitUntilDiscoConfirmed(
+                await test_utils.waitUntilDiscoConfirmed(
                     _converse, _converse.bare_jid,
                     [{'category': 'pubsub', 'type': 'pep'}],
                     ['http://jabber.org/protocol/pubsub#publish-options']
-                ).then(function () {
-                    var IQ_id;
-                    expect(_.filter(_converse.connection.send.calls.all(), function (call) {
-                        var stanza = call.args[0];
-                        if (!(stanza instanceof Element) || stanza.nodeName !== 'iq') {
-                            return;
-                        }
-                        // XXX: Wrapping in a div is a workaround for PhantomJS
-                        var div = document.createElement('div');
-                        div.appendChild(stanza);
-                        if (div.innerHTML ===
-                            '<iq from="dummy@localhost/resource" type="get" '+
-                                'xmlns="jabber:client" id="'+stanza.getAttribute('id')+'">'+
-                            '<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
-                                '<items node="storage:bookmarks"></items>'+
-                            '</pubsub>'+
-                            '</iq>') {
-                            IQ_id = stanza.getAttribute('id');
-                            return true;
+                );
+
+                let IQ_id;
+                const call = await test_utils.waitUntil(() =>
+                    _.filter(
+                        _converse.connection.send.calls.all(),
+                        call => {
+                            const stanza = call.args[0];
+                            if (!(stanza instanceof Element) || stanza.nodeName !== 'iq') {
+                                return;
+                            }
+                            if (sizzle('items[node="storage:bookmarks"]', stanza).length) {
+                                IQ_id = stanza.getAttribute('id');
+                                return true;
+                            }
                         }
-                    }).length).toBe(1);
+                    ).pop()
+                );
+                expect(Strophe.serialize(call.args[0])).toBe(
+                    `<iq from="dummy@localhost/resource" id="${IQ_id}" type="get" xmlns="jabber:client">`+
+                    '<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
+                        '<items node="storage:bookmarks"/>'+
+                    '</pubsub>'+
+                    '</iq>'
+                );
 
-                    var stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':IQ_id})
-                        .c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
-                            .c('items', {'node': 'storage:bookmarks'})
-                                .c('item', {'id': 'current'})
-                                    .c('storage', {'xmlns': 'storage:bookmarks'});
-                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                const stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':IQ_id})
+                    .c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
+                        .c('items', {'node': 'storage:bookmarks'})
+                            .c('item', {'id': 'current'})
+                                .c('storage', {'xmlns': 'storage:bookmarks'});
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
 
-                    _converse.bookmarks.create({
-                        'jid': 'theplay@conference.shakespeare.lit',
-                        'autojoin': false,
-                        'name':  'The Play',
-                        'nick': ''
-                    });
-                    test_utils.waitUntil(() => $('#chatrooms .bookmarks.rooms-list .room-item:visible').length
-                    ).then(function () {
-                        expect($('#chatrooms .bookmarks.rooms-list').hasClass('collapsed')).toBeFalsy();
-                        expect($('#chatrooms .bookmarks.rooms-list .room-item:visible').length).toBe(1);
-                        expect(_converse.bookmarksview.list_model.get('toggle-state')).toBe(_converse.OPENED);
-                        $('#chatrooms .bookmarks-toggle')[0].click();
-                        expect($('#chatrooms .bookmarks.rooms-list').hasClass('collapsed')).toBeTruthy();
-                        expect(_converse.bookmarksview.list_model.get('toggle-state')).toBe(_converse.CLOSED);
-                        $('#chatrooms .bookmarks-toggle')[0].click();
-                        expect($('#chatrooms .bookmarks.rooms-list').hasClass('collapsed')).toBeFalsy();
-                        expect($('#chatrooms .bookmarks.rooms-list .room-item:visible').length).toBe(1);
-                        expect(_converse.bookmarksview.list_model.get('toggle-state')).toBe(_converse.OPENED);
-                        done();
-                    });
+                _converse.bookmarks.create({
+                    'jid': 'theplay@conference.shakespeare.lit',
+                    'autojoin': false,
+                    'name':  'The Play',
+                    'nick': ''
                 });
+                await test_utils.waitUntil(() => $('#chatrooms .bookmarks.rooms-list .room-item:visible').length);
+                expect($('#chatrooms .bookmarks.rooms-list').hasClass('collapsed')).toBeFalsy();
+                expect($('#chatrooms .bookmarks.rooms-list .room-item:visible').length).toBe(1);
+                expect(_converse.bookmarksview.list_model.get('toggle-state')).toBe(_converse.OPENED);
+                $('#chatrooms .bookmarks-toggle')[0].click();
+                expect($('#chatrooms .bookmarks.rooms-list').hasClass('collapsed')).toBeTruthy();
+                expect(_converse.bookmarksview.list_model.get('toggle-state')).toBe(_converse.CLOSED);
+                $('#chatrooms .bookmarks-toggle')[0].click();
+                expect($('#chatrooms .bookmarks.rooms-list').hasClass('collapsed')).toBeFalsy();
+                expect($('#chatrooms .bookmarks.rooms-list .room-item:visible').length).toBe(1);
+                expect(_converse.bookmarksview.list_model.get('toggle-state')).toBe(_converse.OPENED);
+                done();
             }));
         });
     });

+ 80 - 106
spec/chatbox.js

@@ -150,7 +150,8 @@
             }));
 
             it("can be trimmed to conserve space",
-                mock.initConverseWithPromises(null, ['rosterGroupsFetched'], {}, function (done, _converse) {
+                mock.initConverseWithPromises(null, ['rosterGroupsFetched'], {},
+                async function (done, _converse) {
 
                 spyOn(_converse.chatboxviews, 'trimChats');
 
@@ -163,51 +164,47 @@
 
                 test_utils.openControlBox();
 
-                let online_contacts;
-                var i, jid, chatbox, chatboxview, trimmedview;
+                let jid, chatboxview;
                 // openControlBox was called earlier, so the controlbox is
                 // visible, but no other chat boxes have been created.
                 expect(_converse.chatboxes.length).toEqual(1);
                 expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(1); // Controlbox is open
 
                 _converse.rosterview.update(); // XXX: Hack to make sure $roster element is attached.
-                test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length)
-                .then(() => {
-                    // Test that they can be maximized again
-                    online_contacts = _converse.rosterview.el.querySelectorAll('.roster-group .current-xmpp-contact a.open-chat');
-                    expect(online_contacts.length).toBe(15);
-                    for (i=0; i<online_contacts.length; i++) {
-                        const el = online_contacts[i];
-                        el.click();
-                    }
-                    return test_utils.waitUntil(() => _converse.chatboxes.length == 16)
-                }).then(() => {
-                    expect(_converse.chatboxviews.trimChats.calls.count()).toBe(16);
-
-                    for (i=0; i<online_contacts.length; i++) {
-                        const el = online_contacts[i];
-                        jid = _.trim(el.textContent.trim()).replace(/ /g,'.').toLowerCase() + '@localhost';
-                        chatboxview = _converse.chatboxviews.get(jid);
-                        spyOn(chatboxview, 'minimize').and.callThrough();
-                        chatboxview.model.set({'minimized': true});
-                        expect(trimmed_chatboxes.addChat).toHaveBeenCalled();
-                        expect(chatboxview.minimize).toHaveBeenCalled();
-                    }
-                    return test_utils.waitUntil(() => _converse.chatboxviews.keys().length);
-                }).then(function () {
-                    var key = _converse.chatboxviews.keys()[1];
-                    trimmedview = trimmed_chatboxes.get(key);
-                    chatbox = trimmedview.model;
-                    spyOn(chatbox, 'maximize').and.callThrough();
-                    spyOn(trimmedview, 'restore').and.callThrough();
-                    trimmedview.delegateEvents();
-                    trimmedview.el.querySelector("a.restore-chat").click();
-
-                    expect(trimmedview.restore).toHaveBeenCalled();
-                    expect(chatbox.maximize).toHaveBeenCalled();
-                    expect(_converse.chatboxviews.trimChats.calls.count()).toBe(17);
-                    done();
-                });
+                await test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length);
+                // Test that they can be maximized again
+                const online_contacts = _converse.rosterview.el.querySelectorAll('.roster-group .current-xmpp-contact a.open-chat');
+                expect(online_contacts.length).toBe(15);
+                let i;
+                for (i=0; i<online_contacts.length; i++) {
+                    const el = online_contacts[i];
+                    el.click();
+                }
+                await test_utils.waitUntil(() => _converse.chatboxes.length == 16);
+                expect(_converse.chatboxviews.trimChats.calls.count()).toBe(16);
+
+                for (i=0; i<online_contacts.length; i++) {
+                    const el = online_contacts[i];
+                    jid = _.trim(el.textContent.trim()).replace(/ /g,'.').toLowerCase() + '@localhost';
+                    chatboxview = _converse.chatboxviews.get(jid);
+                    spyOn(chatboxview, 'minimize').and.callThrough();
+                    chatboxview.model.set({'minimized': true});
+                    expect(trimmed_chatboxes.addChat).toHaveBeenCalled();
+                    expect(chatboxview.minimize).toHaveBeenCalled();
+                }
+                await test_utils.waitUntil(() => _converse.chatboxviews.keys().length);
+                var key = _converse.chatboxviews.keys()[1];
+                const trimmedview = trimmed_chatboxes.get(key);
+                const chatbox = trimmedview.model;
+                spyOn(chatbox, 'maximize').and.callThrough();
+                spyOn(trimmedview, 'restore').and.callThrough();
+                trimmedview.delegateEvents();
+                trimmedview.el.querySelector("a.restore-chat").click();
+
+                expect(trimmedview.restore).toHaveBeenCalled();
+                expect(chatbox.maximize).toHaveBeenCalled();
+                expect(_converse.chatboxviews.trimChats.calls.count()).toBe(17);
+                done();
             }));
 
             it("can be opened in minimized mode initially",
@@ -453,105 +450,82 @@
                 it("contains a button for inserting emojis",
                     mock.initConverseWithPromises(
                         null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                        function (done, _converse) {
+                        async function (done, _converse) {
 
                     test_utils.createContacts(_converse, 'current');
                     _converse.emit('rosterContactsFetched');
                     test_utils.openControlBox();
 
-                    let timeout = false, view, toolbar;
                     const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
-                    test_utils.openChatBoxFor(_converse, contact_jid)
-                    .then(() => {
-                        view = _converse.chatboxviews.get(contact_jid);
-                        toolbar = view.el.querySelector('ul.chat-toolbar');
-                        expect(toolbar.querySelectorAll('li.toggle-smiley').length).toBe(1);
-                        // Register spies
-                        spyOn(view, 'toggleEmojiMenu').and.callThrough();
-                        spyOn(view, 'insertEmoji').and.callThrough();
-
-                        view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
-                        toolbar.querySelector('li.toggle-smiley').click();
-
-                        return test_utils.waitUntil(() => u.isVisible(view.el.querySelector('.toggle-smiley .emoji-picker-container'), 500));
-                    }).then(() => {
-                        var picker = view.el.querySelector('.toggle-smiley .emoji-picker-container');
-                        var items = picker.querySelectorAll('.emoji-picker li');
-                        items[0].click()
-                        expect(view.insertEmoji).toHaveBeenCalled();
-                        setTimeout(function () { timeout = true; }, 100);
-                        return test_utils.waitUntil(() => timeout, 500);
-                    }).then(() => {
-                        timeout = false;
-                        toolbar.querySelector('li.toggle-smiley').click(); // Close the panel again
-                        return test_utils.waitUntil(() => !view.el.querySelector('.toggle-smiley .toolbar-menu').offsetHeight, 500);
-                    }).then(() => {
-                        setTimeout(function () { timeout = true; }, 100);
-                        return test_utils.waitUntil(() => timeout, 500);
-                    }).then(() => {
-                        toolbar.querySelector('li.toggle-smiley').click();
-                        expect(view.toggleEmojiMenu).toHaveBeenCalled();
-                        return test_utils.waitUntil(() => u.isVisible(view.el.querySelector('.toggle-smiley .emoji-picker-container')), 500);
-                    }).then(() => {
-                        var nodes = view.el.querySelectorAll('.toggle-smiley ul li');
-                        nodes[nodes.length-1].click();
-                        expect(view.el.querySelector('textarea.chat-textarea').value).toBe(':grinning: ');
-                        expect(view.insertEmoji).toHaveBeenCalled();
-                        done();
-                    }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+                    await test_utils.openChatBoxFor(_converse, contact_jid);
+                    const view = _converse.chatboxviews.get(contact_jid);
+                    const toolbar = view.el.querySelector('ul.chat-toolbar');
+                    expect(toolbar.querySelectorAll('li.toggle-smiley').length).toBe(1);
+                    // Register spies
+                    spyOn(view, 'toggleEmojiMenu').and.callThrough();
+                    spyOn(view, 'insertEmoji').and.callThrough();
+
+                    view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
+                    toolbar.querySelector('li.toggle-smiley').click();
+
+                    await test_utils.waitUntil(() => u.isVisible(view.el.querySelector('.toggle-smiley .emoji-picker-container')));
+                    var picker = view.el.querySelector('.toggle-smiley .emoji-picker-container');
+                    var items = picker.querySelectorAll('.emoji-picker li');
+                    items[0].click()
+                    expect(view.insertEmoji).toHaveBeenCalled();
+                    expect(view.el.querySelector('textarea.chat-textarea').value).toBe(':grinning: ');
+                    toolbar.querySelector('li.toggle-smiley').click(); // Close the panel again
+                    done();
                 }));
 
                 it("can contain a button for starting a call",
                     mock.initConverseWithPromises(
                         null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                        function (done, _converse) {
+                        async function (done, _converse) {
 
                     test_utils.createContacts(_converse, 'current');
                     _converse.emit('rosterContactsFetched');
                     test_utils.openControlBox();
 
-                    let view, toolbar, call_button;
+                    let toolbar, call_button;
                     const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
                     spyOn(_converse, 'emit');
                     // First check that the button doesn't show if it's not enabled
                     // via "visible_toolbar_buttons"
                     _converse.visible_toolbar_buttons.call = false;
-                    test_utils.openChatBoxFor(_converse, contact_jid)
-                    .then(() => {
-                        view = _converse.chatboxviews.get(contact_jid);
-                        toolbar = view.el.querySelector('ul.chat-toolbar');
-                        call_button = toolbar.querySelector('.toggle-call');
-                        expect(_.isNull(call_button)).toBeTruthy();
-                        view.close();
-                        // Now check that it's shown if enabled and that it emits
-                        // callButtonClicked
-                        _converse.visible_toolbar_buttons.call = true; // enable the button
-                        return test_utils.openChatBoxFor(_converse, contact_jid);
-                    }).then(() => {
-                        view = _converse.chatboxviews.get(contact_jid);
-                        toolbar = view.el.querySelector('ul.chat-toolbar');
-                        call_button = toolbar.querySelector('.toggle-call');
-                        call_button.click();
-                        expect(_converse.emit).toHaveBeenCalledWith('callButtonClicked', jasmine.any(Object));
-                        done();
-                    });
+                    await test_utils.openChatBoxFor(_converse, contact_jid);
+                    let view = _converse.chatboxviews.get(contact_jid);
+                    toolbar = view.el.querySelector('ul.chat-toolbar');
+                    call_button = toolbar.querySelector('.toggle-call');
+                    expect(_.isNull(call_button)).toBeTruthy();
+                    view.close();
+                    // Now check that it's shown if enabled and that it emits
+                    // callButtonClicked
+                    _converse.visible_toolbar_buttons.call = true; // enable the button
+                    await test_utils.openChatBoxFor(_converse, contact_jid);
+                    view = _converse.chatboxviews.get(contact_jid);
+                    toolbar = view.el.querySelector('ul.chat-toolbar');
+                    call_button = toolbar.querySelector('.toggle-call');
+                    call_button.click();
+                    expect(_converse.emit).toHaveBeenCalledWith('callButtonClicked', jasmine.any(Object));
+                    done();
                 }));
             });
 
             describe("A Chat Status Notification", function () {
 
                 it("does not open a new chatbox",
-                        mock.initConverseWithPromises(
-                            null, ['rosterGroupsFetched'], {},
-                            function (done, _converse) {
+                    mock.initConverseWithPromises(
+                        null, ['rosterGroupsFetched'], {},
+                        function (done, _converse) {
 
                     test_utils.createContacts(_converse, 'current');
                     test_utils.openControlBox();
 
                     spyOn(_converse, 'emit');
-                    var sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
+                    const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
                     // <composing> state
-                    var msg = $msg({
+                    const msg = $msg({
                             'from': sender_jid,
                             'to': _converse.connection.jid,
                             'type': 'chat',

+ 25 - 26
spec/chatroom.js

@@ -2329,35 +2329,34 @@
             }));
 
             it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'",
-                    mock.initConverseWithPromises(
-                        null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                        function (done, _converse) {
+                mock.initConverseWithPromises(
+                    null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                    async function (done, _converse) {
 
-                test_utils.openChatRoom(_converse, 'lounge', 'localhost', 'dummy')
-                .then(() => {
-                    const view = _converse.chatboxviews.get('lounge@localhost'),
-                          trimmed_chatboxes = _converse.minimized_chats;
+                await test_utils.openChatRoom(_converse, 'lounge', 'localhost', 'dummy');
+                const view = _converse.chatboxviews.get('lounge@localhost'),
+                      trimmed_chatboxes = _converse.minimized_chats;
 
-                    spyOn(view, 'minimize').and.callThrough();
-                    spyOn(view, 'maximize').and.callThrough();
-                    spyOn(_converse, 'emit');
-                    view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
-                    view.el.querySelector('.toggle-chatbox-button').click();
-
-                    expect(view.minimize).toHaveBeenCalled();
-                    expect(_converse.emit).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object));
-                    expect(u.isVisible(view.el)).toBeFalsy();
-                    expect(view.model.get('minimized')).toBeTruthy();
-                    expect(view.minimize).toHaveBeenCalled();
-                    var trimmedview = trimmed_chatboxes.get(view.model.get('id'));
-                    trimmedview.el.querySelector("a.restore-chat").click();
-                    expect(view.maximize).toHaveBeenCalled();
-                    expect(_converse.emit).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
-                    expect(view.model.get('minimized')).toBeFalsy();
-                    expect(_converse.emit.calls.count(), 3);
-                    done();
+                spyOn(view, 'minimize').and.callThrough();
+                spyOn(view, 'maximize').and.callThrough();
+                spyOn(_converse, 'emit');
+                view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
+                view.el.querySelector('.toggle-chatbox-button').click();
+
+                expect(view.minimize).toHaveBeenCalled();
+                expect(_converse.emit).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object));
+                expect(u.isVisible(view.el)).toBeFalsy();
+                expect(view.model.get('minimized')).toBeTruthy();
+                expect(view.minimize).toHaveBeenCalled();
+                await test_utils.waitUntil(() => trimmed_chatboxes.get(view.model.get('id')));
+                const trimmedview = trimmed_chatboxes.get(view.model.get('id'));
+                trimmedview.el.querySelector("a.restore-chat").click();
+                expect(view.maximize).toHaveBeenCalled();
+                expect(_converse.emit).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
+                expect(view.model.get('minimized')).toBeFalsy();
+                expect(_converse.emit.calls.count(), 3);
+                done();
 
-                });
             }));
 
             it("can be closed again by clicking a DOM element with class 'close-chatbox-button'",

+ 259 - 263
spec/http-file-upload.js

@@ -14,159 +14,159 @@
 
         describe("Discovering support", function () {
 
-            it("is done automatically", mock.initConverseWithAsync(function (done, _converse) {
+            it("is done automatically", mock.initConverseWithAsync(async function (done, _converse) {
                 var IQ_stanzas = _converse.connection.IQ_stanzas;
                 var IQ_ids =  _converse.connection.IQ_ids;
 
-                test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []).then(function () {
-                    test_utils.waitUntil(function () {
+                await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []);
+                await test_utils.waitUntil(() => _.filter(
+                    IQ_stanzas,
+                    iq => iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]')).length
+                );
+
+                /* <iq type='result'
+                 *      from='plays.shakespeare.lit'
+                 *      to='romeo@montague.net/orchard'
+                 *      id='info1'>
+                 *  <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>
+                 */
+                let stanza = _.find(IQ_stanzas, function (iq) {
+                    return iq.nodeTree.querySelector(
+                        'iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
+                });
+                var info_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
+
+                stanza = $iq({
+                    'type': 'result',
+                    'from': 'localhost',
+                    'to': 'dummy@localhost/resource',
+                    'id': info_IQ_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'});
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                const entities = await _converse.api.disco.entities.get();
+                expect(entities.length).toBe(2);
+                expect(_.includes(entities.pluck('jid'), 'localhost')).toBe(true);
+                expect(_.includes(entities.pluck('jid'), 'dummy@localhost')).toBe(true);
+
+                expect(entities.get(_converse.domain).features.length).toBe(2);
+                expect(entities.get(_converse.domain).identities.length).toBe(1);
+
+                // Converse.js sees that the entity has a disco#items feature,
+                // so it will make a query for it.
+                await test_utils.waitUntil(() => _.filter(
+                        IQ_stanzas,
+                        iq => iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#items"]')
+                    ).length
+                );
+                /* <iq from='montague.tld'
+                 *      id='step_01'
+                 *      to='romeo@montague.tld/garden'
+                 *      type='result'>
+                 *  <query xmlns='http://jabber.org/protocol/disco#items'>
+                 *      <item jid='upload.montague.tld' name='HTTP File Upload' />
+                 *      <item jid='conference.montague.tld' name='Chatroom Service' />
+                 *  </query>
+                 *  </iq>
+                 */
+                stanza = _.find(IQ_stanzas, function (iq) {
+                    return iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#items"]');
+                });
+                var items_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
+                stanza = $iq({
+                    'type': 'result',
+                    'from': 'localhost',
+                    'to': 'dummy@localhost/resource',
+                    'id': items_IQ_id
+                }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#items'})
+                    .c('item', {
+                        'jid': 'upload.localhost',
+                        'name': 'HTTP File Upload'});
+
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                _converse.api.disco.entities.get().then(function (entities) {
+                    expect(entities.length).toBe(2);
+                    expect(entities.get('localhost').items.length).toBe(1);
+                    return test_utils.waitUntil(function () {
+                        // Converse.js sees that the entity has a disco#info feature,
+                        // so it will make a query for it.
                         return _.filter(IQ_stanzas, function (iq) {
-                            return iq.nodeTree.querySelector(
-                                'iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
-                        }).length > 0;
-                    }, 300).then(function () {
-                        /* <iq type='result'
-                         *      from='plays.shakespeare.lit'
-                         *      to='romeo@montague.net/orchard'
-                         *      id='info1'>
-                         *  <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>
-                         */
-                        var stanza = _.find(IQ_stanzas, function (iq) {
-                            return iq.nodeTree.querySelector(
-                                'iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
-                        });
-                        var info_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
-
-                        stanza = $iq({
-                            'type': 'result',
-                            'from': 'localhost',
-                            'to': 'dummy@localhost/resource',
-                            'id': info_IQ_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'});
-                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
-
-                        _converse.api.disco.entities.get().then(function(entities) {
-                            expect(entities.length).toBe(2);
-                            expect(_.includes(entities.pluck('jid'), 'localhost')).toBe(true);
-                            expect(_.includes(entities.pluck('jid'), 'dummy@localhost')).toBe(true);
-
-                            expect(entities.get(_converse.domain).features.length).toBe(2);
-                            expect(entities.get(_converse.domain).identities.length).toBe(1);
-
-                            return test_utils.waitUntil(function () {
-                                // Converse.js sees that the entity has a disco#items feature,
-                                // so it will make a query for it.
-                                return _.filter(IQ_stanzas, function (iq) {
-                                    return iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#items"]');
-                                }).length > 0;
-                            }, 300);
-                        });
-                    }).then(function () {
-                        /* <iq from='montague.tld'
-                         *      id='step_01'
-                         *      to='romeo@montague.tld/garden'
-                         *      type='result'>
-                         *  <query xmlns='http://jabber.org/protocol/disco#items'>
-                         *      <item jid='upload.montague.tld' name='HTTP File Upload' />
-                         *      <item jid='conference.montague.tld' name='Chatroom Service' />
-                         *  </query>
-                         *  </iq>
-                         */
-                    var stanza = _.find(IQ_stanzas, function (iq) {
-                        return iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#items"]');
-                    });
-                    var items_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
-                    stanza = $iq({
-                        'type': 'result',
-                        'from': 'localhost',
-                        'to': 'dummy@localhost/resource',
-                        'id': items_IQ_id
-                    }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#items'})
-                        .c('item', {
-                            'jid': 'upload.localhost',
-                            'name': 'HTTP File Upload'});
-                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
-
-                        _converse.api.disco.entities.get().then(function (entities) {
-                            expect(entities.length).toBe(2);
-                            expect(entities.get('localhost').items.length).toBe(1);
-                            return test_utils.waitUntil(function () {
-                                // Converse.js sees that the entity has a disco#info feature,
-                                // so it will make a query for it.
-                                return _.filter(IQ_stanzas, function (iq) {
-                                    return iq.nodeTree.querySelector('iq[to="upload.localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
-                                }).length > 0;
-                            }, 300);
-                        });
-                    }).then(function () {
-                        var stanza = _.find(IQ_stanzas, function (iq) {
                             return iq.nodeTree.querySelector('iq[to="upload.localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
-                        });
-                        var IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
-                        expect(stanza.toLocaleString()).toBe(
-                            `<iq from="dummy@localhost/resource" id="`+IQ_id+`" to="upload.localhost" type="get" xmlns="jabber:client">`+
-                                `<query xmlns="http://jabber.org/protocol/disco#info"/>`+
-                            `</iq>`);
-
-                        // Upload service responds and reports a maximum file size of 5MiB
-                        /* <iq from='upload.montague.tld'
-                         *     id='step_02'
-                         *     to='romeo@montague.tld/garden'
-                         *     type='result'>
-                         * <query xmlns='http://jabber.org/protocol/disco#info'>
-                         *     <identity category='store'
-                         *             type='file'
-                         *             name='HTTP File Upload' />
-                         *     <feature var='urn:xmpp:http:upload:0' />
-                         *     <x type='result' xmlns='jabber:x:data'>
-                         *     <field var='FORM_TYPE' type='hidden'>
-                         *         <value>urn:xmpp:http:upload:0</value>
-                         *     </field>
-                         *     <field var='max-file-size'>
-                         *         <value>5242880</value>
-                         *     </field>
-                         *     </x>
-                         * </query>
-                         * </iq>
-                         */
-                        stanza = $iq({'type': 'result', 'to': 'dummy@localhost/resource', 'id': IQ_id, 'from': 'upload.localhost'})
-                            .c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
-                                .c('identity', {'category':'store', 'type':'file', 'name':'HTTP File Upload'}).up()
-                                .c('feature', {'var':'urn:xmpp:http:upload:0'}).up()
-                                .c('x', {'type':'result', 'xmlns':'jabber:x:data'})
-                                    .c('field', {'var':'FORM_TYPE', 'type':'hidden'})
-                                        .c('value').t('urn:xmpp:http:upload:0').up().up()
-                                    .c('field', {'var':'max-file-size'})
-                                        .c('value').t('5242880');
-                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                        }).length > 0;
+                    }, 300);
+                });
 
-                        _converse.api.disco.entities.get().then(function (entities) {
-                            expect(entities.get('localhost').items.get('upload.localhost').identities.where({'category': 'store'}).length).toBe(1);
-                            _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain).then(
-                                function (result) {
-                                    expect(result.length).toBe(1);
-                                    expect(result[0].get('jid')).toBe('upload.localhost');
-                                    expect(result[0].dataforms.where({'FORM_TYPE': {value: "urn:xmpp:http:upload:0", type: "hidden"}}).length).toBe(1);
-                                    done();
-                                }
-                            );
-                        }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
-                    })
-                })
+                stanza = await test_utils.waitUntil(() =>
+                    _.filter(
+                        IQ_stanzas,
+                        iq => iq.nodeTree.querySelector('iq[to="upload.localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]')
+                    ).pop()
+                );
+                const IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
+
+                expect(stanza.toLocaleString()).toBe(
+                    `<iq from="dummy@localhost/resource" id="`+IQ_id+`" to="upload.localhost" type="get" xmlns="jabber:client">`+
+                        `<query xmlns="http://jabber.org/protocol/disco#info"/>`+
+                    `</iq>`);
+
+                // Upload service responds and reports a maximum file size of 5MiB
+                /* <iq from='upload.montague.tld'
+                 *     id='step_02'
+                 *     to='romeo@montague.tld/garden'
+                 *     type='result'>
+                 * <query xmlns='http://jabber.org/protocol/disco#info'>
+                 *     <identity category='store'
+                 *             type='file'
+                 *             name='HTTP File Upload' />
+                 *     <feature var='urn:xmpp:http:upload:0' />
+                 *     <x type='result' xmlns='jabber:x:data'>
+                 *     <field var='FORM_TYPE' type='hidden'>
+                 *         <value>urn:xmpp:http:upload:0</value>
+                 *     </field>
+                 *     <field var='max-file-size'>
+                 *         <value>5242880</value>
+                 *     </field>
+                 *     </x>
+                 * </query>
+                 * </iq>
+                 */
+                stanza = $iq({'type': 'result', 'to': 'dummy@localhost/resource', 'id': IQ_id, 'from': 'upload.localhost'})
+                    .c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
+                        .c('identity', {'category':'store', 'type':'file', 'name':'HTTP File Upload'}).up()
+                        .c('feature', {'var':'urn:xmpp:http:upload:0'}).up()
+                        .c('x', {'type':'result', 'xmlns':'jabber:x:data'})
+                            .c('field', {'var':'FORM_TYPE', 'type':'hidden'})
+                                .c('value').t('urn:xmpp:http:upload:0').up().up()
+                            .c('field', {'var':'max-file-size'})
+                                .c('value').t('5242880');
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                _converse.api.disco.entities.get().then(function (entities) {
+                    expect(entities.get('localhost').items.get('upload.localhost').identities.where({'category': 'store'}).length).toBe(1);
+                    _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain).then(
+                        function (result) {
+                            expect(result.length).toBe(1);
+                            expect(result[0].get('jid')).toBe('upload.localhost');
+                            expect(result[0].dataforms.where({'FORM_TYPE': {value: "urn:xmpp:http:upload:0", type: "hidden"}}).length).toBe(1);
+                            done();
+                        }
+                    );
+                }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
             }));
         });
 
@@ -474,128 +474,124 @@
                         done();
                     }));
 
-                    it("shows an error message if the file is too large", mock.initConverseWithAsync(function (done, _converse) {
+                    it("shows an error message if the file is too large", mock.initConverseWithAsync(async function (done, _converse) {
                         const IQ_stanzas = _converse.connection.IQ_stanzas;
                         const IQ_ids =  _converse.connection.IQ_ids;
                         const send_backup = XMLHttpRequest.prototype.send;
-                        let view, contact_jid;
-
-                        test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [])
-                        .then(() => test_utils.waitUntil(() => _.filter(
-                            IQ_stanzas, (iq) => iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]')).length
-                        )).then(() => {
-                            var stanza = _.find(IQ_stanzas, function (iq) {
-                                return iq.nodeTree.querySelector(
-                                    'iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
-                            });
-                            var info_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
-
-                            stanza = $iq({
-                                'type': 'result',
-                                'from': 'localhost',
-                                'to': 'dummy@localhost/resource',
-                                'id': info_IQ_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'});
-                            _converse.connection._dataRecv(test_utils.createRequest(stanza));
-                            return _converse.api.disco.entities.get();
-                        }).then(function (entities) {
-                            expect(entities.length).toBe(2);
-                            expect(_.includes(entities.pluck('jid'), 'localhost')).toBe(true);
-                            expect(_.includes(entities.pluck('jid'), 'dummy@localhost')).toBe(true);
-
-                            expect(entities.get(_converse.domain).features.length).toBe(2);
-                            expect(entities.get(_converse.domain).identities.length).toBe(1);
-
-                            return test_utils.waitUntil(function () {
-                                // Converse.js sees that the entity has a disco#items feature,
-                                // so it will make a query for it.
-                                return _.filter(IQ_stanzas, function (iq) {
-                                    return iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#items"]');
-                                }).length > 0;
-                            }, 300);
-                        }).then(function () {
-                            var stanza = _.find(IQ_stanzas, function (iq) {
+
+                        await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []);
+                        await test_utils.waitUntil(() => _.filter(
+                            IQ_stanzas,
+                            iq => iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]')).length
+                        );
+
+                        var stanza = _.find(IQ_stanzas, function (iq) {
+                            return iq.nodeTree.querySelector(
+                                'iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
+                        });
+                        var info_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
+
+                        stanza = $iq({
+                            'type': 'result',
+                            'from': 'localhost',
+                            'to': 'dummy@localhost/resource',
+                            'id': info_IQ_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'});
+                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                        let entities = await _converse.api.disco.entities.get();
+
+                        expect(entities.length).toBe(2);
+                        expect(_.includes(entities.pluck('jid'), 'localhost')).toBe(true);
+                        expect(_.includes(entities.pluck('jid'), 'dummy@localhost')).toBe(true);
+
+                        expect(entities.get(_converse.domain).features.length).toBe(2);
+                        expect(entities.get(_converse.domain).identities.length).toBe(1);
+
+                        await test_utils.waitUntil(function () {
+                            // Converse.js sees that the entity has a disco#items feature,
+                            // so it will make a query for it.
+                            return _.filter(IQ_stanzas, function (iq) {
                                 return iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#items"]');
-                            });
-                            var items_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
-                            stanza = $iq({
-                                'type': 'result',
-                                'from': 'localhost',
-                                'to': 'dummy@localhost/resource',
-                                'id': items_IQ_id
-                            }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#items'})
-                                .c('item', {
-                                    'jid': 'upload.localhost',
-                                    'name': 'HTTP File Upload'});
-
-                            _converse.connection._dataRecv(test_utils.createRequest(stanza));
-
-                            _converse.api.disco.entities.get().then(function (entities) {
-                                expect(entities.length).toBe(2);
-                                expect(entities.get('localhost').items.length).toBe(1);
-                                return test_utils.waitUntil(function () {
-                                    // Converse.js sees that the entity has a disco#info feature,
-                                    // so it will make a query for it.
-                                    return _.filter(IQ_stanzas, function (iq) {
-                                        return iq.nodeTree.querySelector('iq[to="upload.localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
-                                    }).length > 0;
-                                }, 300);
-                            });
-                        }).then(function () {
-                            var stanza = _.find(IQ_stanzas, function (iq) {
+                            }).length > 0;
+                        }, 300);
+
+                        stanza = _.find(IQ_stanzas, function (iq) {
+                            return iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#items"]');
+                        });
+                        var items_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
+                        stanza = $iq({
+                            'type': 'result',
+                            'from': 'localhost',
+                            'to': 'dummy@localhost/resource',
+                            'id': items_IQ_id
+                        }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#items'})
+                            .c('item', {
+                                'jid': 'upload.localhost',
+                                'name': 'HTTP File Upload'});
+
+                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                        entities = await _converse.api.disco.entities.get()
+
+                        expect(entities.length).toBe(2);
+                        expect(entities.get('localhost').items.length).toBe(1);
+                        await test_utils.waitUntil(function () {
+                            // Converse.js sees that the entity has a disco#info feature,
+                            // so it will make a query for it.
+                            return _.filter(IQ_stanzas, function (iq) {
                                 return iq.nodeTree.querySelector('iq[to="upload.localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
-                            });
-                            var IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
-                            expect(stanza.toLocaleString()).toBe(
-                                `<iq from="dummy@localhost/resource" id="${IQ_id}" to="upload.localhost" type="get" xmlns="jabber:client">`+
-                                    `<query xmlns="http://jabber.org/protocol/disco#info"/>`+
-                                `</iq>`);
-
-                            // Upload service responds and reports a maximum file size of 5MiB
-                            stanza = $iq({'type': 'result', 'to': 'dummy@localhost/resource', 'id': IQ_id, 'from': 'upload.localhost'})
-                                .c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
-                                    .c('identity', {'category':'store', 'type':'file', 'name':'HTTP File Upload'}).up()
-                                    .c('feature', {'var':'urn:xmpp:http:upload:0'}).up()
-                                    .c('x', {'type':'result', 'xmlns':'jabber:x:data'})
-                                        .c('field', {'var':'FORM_TYPE', 'type':'hidden'})
-                                            .c('value').t('urn:xmpp:http:upload:0').up().up()
-                                        .c('field', {'var':'max-file-size'})
-                                            .c('value').t('5242880');
-                            _converse.connection._dataRecv(test_utils.createRequest(stanza));
-                            return _converse.api.disco.entities.get();
-                        }).then(function (entities) {
-                            expect(entities.get('localhost').items.get('upload.localhost').identities.where({'category': 'store'}).length).toBe(1);
-                            return _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain);
-                        }).then(function (result) {
-                            test_utils.createContacts(_converse, 'current');
-                            _converse.emit('rosterContactsFetched');
+                            }).length > 0;
+                        }, 300);
 
-                            contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
-                            return test_utils.openChatBoxFor(_converse, contact_jid);
-                        }).then(() => {
-                            view = _converse.chatboxviews.get(contact_jid);
-                            var file = {
-                                'type': 'image/jpeg',
-                                'size': '5242881',
-                                'lastModifiedDate': "",
-                                'name': "my-juliet.jpg"
-                            };
-                            view.model.sendFiles([file]);
-                            return test_utils.waitUntil(() => view.el.querySelectorAll('.message').length)
-                        }).then(function () {
-                            const messages = view.el.querySelectorAll('.message.chat-error');
-                            expect(messages.length).toBe(1);
-                            expect(messages[0].textContent).toBe(
-                                'The size of your file, my-juliet.jpg, exceeds the maximum allowed by your server, which is 5 MB.');
-                            done();
-                        }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL))
+                        stanza = _.find(IQ_stanzas, function (iq) {
+                            return iq.nodeTree.querySelector('iq[to="upload.localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
+                        });
+                        var IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
+                        expect(stanza.toLocaleString()).toBe(
+                            `<iq from="dummy@localhost/resource" id="${IQ_id}" to="upload.localhost" type="get" xmlns="jabber:client">`+
+                                `<query xmlns="http://jabber.org/protocol/disco#info"/>`+
+                            `</iq>`);
+
+                        // Upload service responds and reports a maximum file size of 5MiB
+                        stanza = $iq({'type': 'result', 'to': 'dummy@localhost/resource', 'id': IQ_id, 'from': 'upload.localhost'})
+                            .c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
+                                .c('identity', {'category':'store', 'type':'file', 'name':'HTTP File Upload'}).up()
+                                .c('feature', {'var':'urn:xmpp:http:upload:0'}).up()
+                                .c('x', {'type':'result', 'xmlns':'jabber:x:data'})
+                                    .c('field', {'var':'FORM_TYPE', 'type':'hidden'})
+                                        .c('value').t('urn:xmpp:http:upload:0').up().up()
+                                    .c('field', {'var':'max-file-size'})
+                                        .c('value').t('5242880');
+                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                        entities = await _converse.api.disco.entities.get();
+                        expect(entities.get('localhost').items.get('upload.localhost').identities.where({'category': 'store'}).length).toBe(1);
+                        const result = await _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain);
+                        test_utils.createContacts(_converse, 'current');
+                        _converse.emit('rosterContactsFetched');
+
+                        const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
+                        await test_utils.openChatBoxFor(_converse, contact_jid);
+                        const view = _converse.chatboxviews.get(contact_jid);
+                        var file = {
+                            'type': 'image/jpeg',
+                            'size': '5242881',
+                            'lastModifiedDate': "",
+                            'name': "my-juliet.jpg"
+                        };
+                        view.model.sendFiles([file]);
+                        await test_utils.waitUntil(() => view.el.querySelectorAll('.message').length)
+                        const messages = view.el.querySelectorAll('.message.chat-error');
+                        expect(messages.length).toBe(1);
+                        expect(messages[0].textContent).toBe(
+                            'The size of your file, my-juliet.jpg, exceeds the maximum allowed by your server, which is 5 MB.');
+                        done();
                     }));
                 });
             });

+ 42 - 39
spec/notification.js

@@ -159,47 +159,50 @@
                 it("is played when the current user is mentioned in a chat room",
                     mock.initConverseWithPromises(
                         null, ['rosterGroupsFetched'], {},
-                        function (done, _converse) {
+                        async function (done, _converse) {
 
                     test_utils.createContacts(_converse, 'current');
-                    test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy').then(function () {
-                        _converse.play_sounds = true;
-                        spyOn(_converse, 'playSoundNotification');
-                        var view = _converse.chatboxviews.get('lounge@localhost');
-                        if (!$(view.el).find('.chat-area').length) { view.renderChatArea(); }
-                        var text = 'This message will play a sound because it mentions dummy';
-                        var message = $msg({
-                            from: 'lounge@localhost/otheruser',
-                            id: '1',
-                            to: 'dummy@localhost',
-                            type: 'groupchat'
-                        }).c('body').t(text);
-                        view.model.onMessage(message.nodeTree);
-                        expect(_converse.playSoundNotification).toHaveBeenCalled();
-
-                        text = "This message won't play a sound";
-                        message = $msg({
-                            from: 'lounge@localhost/otheruser',
-                            id: '2',
-                            to: 'dummy@localhost',
-                            type: 'groupchat'
-                        }).c('body').t(text);
-                        view.model.onMessage(message.nodeTree);
-                        expect(_converse.playSoundNotification, 1);
-                        _converse.play_sounds = false;
-
-                        text = "This message won't play a sound because it is sent by dummy";
-                        message = $msg({
-                            from: 'lounge@localhost/dummy',
-                            id: '3',
-                            to: 'dummy@localhost',
-                            type: 'groupchat'
-                        }).c('body').t(text);
-                        view.model.onMessage(message.nodeTree);
-                        expect(_converse.playSoundNotification, 1);
-                        _converse.play_sounds = false;
-                        done();
-                    });
+                    await test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy');
+                    _converse.play_sounds = true;
+                    spyOn(_converse, 'playSoundNotification');
+                    const view = _converse.chatboxviews.get('lounge@localhost');
+                    if (!view.el.querySelectorAll('.chat-area').length) {
+                        view.renderChatArea();
+                    }
+                    let text = 'This message will play a sound because it mentions dummy';
+                    let message = $msg({
+                        from: 'lounge@localhost/otheruser',
+                        id: '1',
+                        to: 'dummy@localhost',
+                        type: 'groupchat'
+                    }).c('body').t(text);
+                    view.model.onMessage(message.nodeTree);
+
+                    await test_utils.waitUntil(() => _converse.playSoundNotification.calls.count());
+                    expect(_converse.playSoundNotification).toHaveBeenCalled();
+
+                    text = "This message won't play a sound";
+                    message = $msg({
+                        from: 'lounge@localhost/otheruser',
+                        id: '2',
+                        to: 'dummy@localhost',
+                        type: 'groupchat'
+                    }).c('body').t(text);
+                    view.model.onMessage(message.nodeTree);
+                    expect(_converse.playSoundNotification, 1);
+                    _converse.play_sounds = false;
+
+                    text = "This message won't play a sound because it is sent by dummy";
+                    message = $msg({
+                        from: 'lounge@localhost/dummy',
+                        id: '3',
+                        to: 'dummy@localhost',
+                        type: 'groupchat'
+                    }).c('body').t(text);
+                    view.model.onMessage(message.nodeTree);
+                    expect(_converse.playSoundNotification, 1);
+                    _converse.play_sounds = false;
+                    done();
                 }));
             });
         });

+ 18 - 31
spec/roster.js

@@ -10,28 +10,21 @@
     const u = converse.env.utils;
 
 
-    const checkHeaderToggling = function (group) {
-        var $group = $(group);
+    const checkHeaderToggling = async function (group) {
         var toggle = group.querySelector('a.group-toggle');
-        expect(u.isVisible($group[0])).toBeTruthy();
-        expect($group.find('ul.collapsed').length).toBe(0);
+        expect(u.isVisible(group)).toBeTruthy();
+        expect(group.querySelectorAll('ul.collapsed').length).toBe(0);
         expect(u.hasClass('fa-caret-right', toggle.firstElementChild)).toBeFalsy();
         expect(u.hasClass('fa-caret-down', toggle.firstElementChild)).toBeTruthy();
         toggle.click();
 
-        return test_utils.waitUntil(function () {
-            return $group.find('ul.collapsed').length === 1;
-        }, 500).then(function () {
-            expect(u.hasClass('fa-caret-right', toggle.firstElementChild)).toBeTruthy();
-            expect(u.hasClass('fa-caret-down', toggle.firstElementChild)).toBeFalsy();
-            toggle.click();
-            return test_utils.waitUntil(function () {
-                return $group.find('li').length === $group.find('li:visible').length
-            }, 500);
-        }).then(function () {
-            expect(u.hasClass('fa-caret-right', toggle.firstElementChild)).toBeFalsy();
-            expect(u.hasClass('fa-caret-down', toggle.firstElementChild)).toBeTruthy();
-        });
+        await test_utils.waitUntil(() => group.querySelectorAll('ul.collapsed').length === 1);
+        expect(u.hasClass('fa-caret-right', toggle.firstElementChild)).toBeTruthy();
+        expect(u.hasClass('fa-caret-down', toggle.firstElementChild)).toBeFalsy();
+        toggle.click();
+        await test_utils.waitUntil(() => group.querySelectorAll('li').length === $(group).find('li:visible').length);
+        expect(u.hasClass('fa-caret-right', toggle.firstElementChild)).toBeFalsy();
+        expect(u.hasClass('fa-caret-down', toggle.firstElementChild)).toBeTruthy();
     };
 
 
@@ -501,7 +494,7 @@
             it("remembers whether it is closed or opened",
                 mock.initConverseWithPromises(
                     null, ['rosterGroupsFetched'], {},
-                    function (done, _converse) {
+                    async function (done, _converse) {
 
                 _converse.roster_groups = true;
                 test_utils.openControlBox();
@@ -524,20 +517,14 @@
                         });
                     }
                 });
-                var view = _converse.rosterview.get('colleagues');
-                var $toggle = $(view.el).find('a.group-toggle');
+                const view = _converse.rosterview.get('colleagues');
+                const toggle = view.el.querySelector('a.group-toggle');
                 expect(view.model.get('state')).toBe('opened');
-                $toggle[0].click();
-                return test_utils.waitUntil(function () {
-                    return view.model.get('state') === 'closed';
-                }, 500).then(function () {
-                    $toggle[0].click();
-                    return test_utils.waitUntil(function () {
-                        return view.model.get('state') === 'opened';
-                    }, 500)
-                }).then(function () {
-                    done();
-                });
+                toggle.click();
+                await test_utils.waitUntil(() => view.model.get('state') === 'closed');
+                toggle.click();
+                await test_utils.waitUntil(() => view.model.get('state') === 'opened');
+                done();
             }));
         });
 

+ 3 - 0
spec/spoilers.js

@@ -105,6 +105,7 @@
             spyOn(view, 'onMessageSubmitted').and.callThrough();
             spyOn(_converse.connection, 'send');
 
+            await test_utils.waitUntil(() => view.el.querySelector('.toggle-compose-spoiler'));
             let spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler');
             spoiler_toggle.click();
 
@@ -178,6 +179,8 @@
             await test_utils.openChatBoxFor(_converse, contact_jid);
             await test_utils.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]);
             const view = _converse.chatboxviews.get(contact_jid);
+
+            await test_utils.waitUntil(() => view.el.querySelector('.toggle-compose-spoiler'));
             let spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler');
             spoiler_toggle.click();
 

+ 19 - 27
src/converse-bookmarks.js

@@ -69,15 +69,14 @@ converse.plugins.add('converse-bookmarks', {
                 close_button.insertAdjacentHTML('afterend', bookmark_button);
             },
 
-            renderHeading () {
+            async renderHeading () {
                 this.__super__.renderHeading.apply(this, arguments);
                 const { _converse } = this.__super__;
                 if (_converse.allow_bookmarks) {
-                    _converse.checkBookmarksSupport().then((supported) => {
-                        if (supported) {
-                            this.renderBookmarkToggle();
-                        }
-                    }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+                    const supported = await _converse.checkBookmarksSupport();
+                    if (supported) {
+                        this.renderBookmarkToggle();
+                    }
                 }
             },
 
@@ -518,32 +517,25 @@ converse.plugins.add('converse-bookmarks', {
             }
         });
 
-        _converse.checkBookmarksSupport = function () {
-            return new Promise((resolve, reject) => {
-                Promise.all([
-                    _converse.api.disco.getIdentity('pubsub', 'pep', _converse.bare_jid),
-                    _converse.api.disco.supports(Strophe.NS.PUBSUB+'#publish-options', _converse.bare_jid)
-                ]).then((args) => {
-                    resolve(args[0] && (args[1].length || _converse.allow_public_bookmarks));
-                }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
-            }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+        _converse.checkBookmarksSupport = async function () {
+            const args = await Promise.all([
+                _converse.api.disco.getIdentity('pubsub', 'pep', _converse.bare_jid),
+                _converse.api.disco.supports(Strophe.NS.PUBSUB+'#publish-options', _converse.bare_jid)
+            ]);
+            return args[0] && (args[1].length || _converse.allow_public_bookmarks);
         }
 
-        const initBookmarks = function () {
+        const initBookmarks = async function () {
             if (!_converse.allow_bookmarks) {
                 return;
             }
-            _converse.checkBookmarksSupport().then((supported) => {
-                if (supported) {
-                    _converse.bookmarks = new _converse.Bookmarks();
-                    _converse.bookmarksview = new _converse.BookmarksView({'model': _converse.bookmarks});
-                    _converse.bookmarks.fetchBookmarks()
-                        .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL))
-                        .then(() => _converse.emit('bookmarksInitialized'));
-                } else {
-                    _converse.emit('bookmarksInitialized');
-                }
-            });
+            const supported = await _converse.checkBookmarksSupport();
+            if (supported) {
+                _converse.bookmarks = new _converse.Bookmarks();
+                _converse.bookmarksview = new _converse.BookmarksView({'model': _converse.bookmarks});
+                await _converse.bookmarks.fetchBookmarks();
+            }
+            _converse.emit('bookmarksInitialized');
         }
 
         u.onMultipleEvents([

+ 33 - 24
src/converse-chatview.js

@@ -10,6 +10,7 @@ import "converse-modal";
 import * as twemoji from "twemoji";
 import bootstrap from "bootstrap";
 import converse from "@converse/headless/converse-core";
+import tpl_alert from "templates/alert.html";
 import tpl_chatbox from "templates/chatbox.html";
 import tpl_chatbox_head from "templates/chatbox_head.html";
 import tpl_chatbox_message_form from "templates/chatbox_message_form.html";
@@ -229,13 +230,23 @@ converse.plugins.add('converse-chatview', {
                 }
             },
 
-            refreshContact (ev) {
+            async refreshContact (ev) {
                 if (ev && ev.preventDefault) { ev.preventDefault(); }
                 const refresh_icon = this.el.querySelector('.fa-refresh');
                 u.addClass('fa-spin', refresh_icon);
-                _converse.api.vcard.update(this.model.contact.vcard, true)
-                    .then(() => u.removeClass('fa-spin', refresh_icon))
-                    .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+                try {
+                    await _converse.api.vcard.update(this.model.contact.vcard, true);
+                } catch (e) {
+                    _converse.log(e, Strophe.LogLevel.FATAL);
+                    this.el.querySelector('.modal-body').insertAdjacentHTML(
+                        'afterBegin',
+                        tpl_alert({
+                            'type': 'alert-danger',
+                            'message': __('Sorry, something went wrong while trying to refresh')
+                        })
+                    );
+                }
+                u.removeClass('fa-spin', refresh_icon);
             },
 
             removeContact (ev) {
@@ -387,17 +398,16 @@ converse.plugins.add('converse-chatview', {
                 this.model.sendFiles(evt.target.files);
             },
 
-            addFileUploadButton (options) {
-                _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain).then((result) => {
-                    if (result.length) {
-                        this.el.querySelector('.chat-toolbar').insertAdjacentHTML(
-                            'beforeend',
-                            tpl_toolbar_fileupload({'tooltip_upload_file': __('Choose a file to send')}));
-                    }
-                }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+            async addFileUploadButton (options) {
+                const result = await _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain);
+                if (result.length) {
+                    this.el.querySelector('.chat-toolbar').insertAdjacentHTML(
+                        'beforeend',
+                        tpl_toolbar_fileupload({'tooltip_upload_file': __('Choose a file to send')}));
+                }
             },
 
-            addSpoilerButton (options) {
+            async addSpoilerButton (options) {
                 /* Asynchronously adds a button for writing spoiler
                  * messages, based on whether the contact's client supports
                  * it.
@@ -410,18 +420,17 @@ converse.plugins.add('converse-chatview', {
                 if (_.isEmpty(resources)) {
                     return;
                 }
-                Promise.all(_.map(_.keys(resources), (resource) =>
-                    _converse.api.disco.supports(Strophe.NS.SPOILER, `${contact_jid}/${resource}`)
-                )).then((results) => {
-                    if (_.filter(results, 'length').length) {
-                        const html = tpl_spoiler_button(this.model.toJSON());
-                        if (_converse.visible_toolbar_buttons.emoji) {
-                            this.el.querySelector('.toggle-smiley').insertAdjacentHTML('afterEnd', html);
-                        } else {
-                            this.el.querySelector('.chat-toolbar').insertAdjacentHTML('afterBegin', html);
-                        }
+                const results = await Promise.all(_.map(_.keys(resources),
+                    resource => _converse.api.disco.supports(Strophe.NS.SPOILER, `${contact_jid}/${resource}`)
+                ));
+                if (_.filter(results, 'length').length) {
+                    const html = tpl_spoiler_button(this.model.toJSON());
+                    if (_converse.visible_toolbar_buttons.emoji) {
+                        this.el.querySelector('.toggle-smiley').insertAdjacentHTML('afterEnd', html);
+                    } else {
+                        this.el.querySelector('.chat-toolbar').insertAdjacentHTML('afterBegin', html);
                     }
-                }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+                }
             },
 
             insertHeading () {

+ 26 - 27
src/converse-minimize.js

@@ -229,7 +229,7 @@ converse.plugins.add('converse-minimize', {
                 );
             },
 
-            trimChats (newchat) {
+            async trimChats (newchat) {
                 /* This method is called when a newly created chat box will
                  * be shown.
                  *
@@ -250,35 +250,34 @@ converse.plugins.add('converse-minimize', {
                     // fullscreen. In this case we don't trim.
                     return;
                 }
-                _converse.api.waitUntil('minimizedChatsInitialized').then(() => {
-                    const minimized_el = _.get(_converse.minimized_chats, 'el'),
-                          new_id = newchat ? newchat.model.get('id') : null;
-
-                    if (minimized_el) {
-                        const minimized_width = _.includes(this.model.pluck('minimized'), true) ?
-                            u.getOuterWidth(minimized_el, true) : 0;
-
-                        const boxes_width = _.reduce(
-                            this.xget(new_id),
-                            (memo, view) => memo + this.getChatBoxWidth(view),
-                            newchat ? u.getOuterWidth(newchat.el, true) : 0
-                        );
-                        if ((minimized_width + boxes_width) > body_width) {
-                            const oldest_chat = this.getOldestMaximizedChat([new_id]);
-                            if (oldest_chat) {
-                                // We hide the chat immediately, because waiting
-                                // for the event to fire (and letting the
-                                // ChatBoxView hide it then) causes race
-                                // conditions.
-                                const view = this.get(oldest_chat.get('id'));
-                                if (view) {
-                                    view.hide();
-                                }
-                                oldest_chat.minimize();
+                await _converse.api.waitUntil('minimizedChatsInitialized');
+                const minimized_el = _.get(_converse.minimized_chats, 'el'),
+                      new_id = newchat ? newchat.model.get('id') : null;
+
+                if (minimized_el) {
+                    const minimized_width = _.includes(this.model.pluck('minimized'), true) ?
+                        u.getOuterWidth(minimized_el, true) : 0;
+
+                    const boxes_width = _.reduce(
+                        this.xget(new_id),
+                        (memo, view) => memo + this.getChatBoxWidth(view),
+                        newchat ? u.getOuterWidth(newchat.el, true) : 0
+                    );
+                    if ((minimized_width + boxes_width) > body_width) {
+                        const oldest_chat = this.getOldestMaximizedChat([new_id]);
+                        if (oldest_chat) {
+                            // We hide the chat immediately, because waiting
+                            // for the event to fire (and letting the
+                            // ChatBoxView hide it then) causes race
+                            // conditions.
+                            const view = this.get(oldest_chat.get('id'));
+                            if (view) {
+                                view.hide();
                             }
+                            oldest_chat.minimize();
                         }
                     }
-                }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+                }
             },
 
             getOldestMaximizedChat (exclude_ids) {

+ 2 - 2
src/converse-muc-views.js

@@ -258,7 +258,7 @@ converse.plugins.add('converse-muc-views', {
             } else {
                 parent_el.insertAdjacentHTML('beforeend', tpl_spinner());
                 _converse.api.disco.info(ev.target.getAttribute('data-room-jid'), null)
-                    .then((stanza) => insertRoomInfo(parent_el, stanza))
+                    .then(stanza => insertRoomInfo(parent_el, stanza))
                     .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
             }
         }
@@ -1159,7 +1159,7 @@ converse.plugins.add('converse-muc-views', {
                  */
                 this.showSpinner();
                 this.model.fetchRoomConfiguration()
-                    .then(this.renderConfigurationForm.bind(this))
+                    .then(iq => this.renderConfigurationForm(iq))
                     .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
             },
 

+ 9 - 12
src/converse-oauth.js

@@ -106,17 +106,16 @@ converse.plugins.add("converse-oauth", {
                     }));
             },
 
-            fetchOAuthProfileDataAndLogin () {
-                this.oauth_service.api('me').then((profile) => {
-                    const response = this.oauth_service.getAuthResponse();
-                    _converse.api.user.login({
-                        'jid': `${profile.name}@${this.provider.get('host')}`,
-                        'password': response.access_token
-                    });
+            async fetchOAuthProfileDataAndLogin () {
+                const profile = await this.oauth_service.api('me');
+                const response = this.oauth_service.getAuthResponse();
+                _converse.api.user.login({
+                    'jid': `${profile.name}@${this.provider.get('host')}`,
+                    'password': response.access_token
                 });
             },
 
-            oauthLogin (ev) {
+            async oauthLogin (ev) {
                 ev.preventDefault();
                 const id = ev.target.getAttribute('data-id');
                 this.provider = _converse.oauth_providers.get(id);
@@ -128,10 +127,8 @@ converse.plugins.add("converse-oauth", {
                     'redirect_uri': '/redirect.html'
                 });
 
-                this.oauth_service.login().then(
-                    () => this.fetchOAuthProfileDataAndLogin(),
-                    (error) => _converse.log(error.error_message, Strophe.LogLevel.ERROR)
-                );
+                await this.oauth_service.login();
+                this.fetchOAuthProfileDataAndLogin();
             }
         });
     }

+ 0 - 1
src/converse-profile.js

@@ -11,7 +11,6 @@ import "converse-modal";
 import _FormData from "formdata-polyfill";
 import bootstrap from "bootstrap";
 import converse from "@converse/headless/converse-core";
-import tpl_alert from "templates/alert.html";
 import tpl_chat_status_modal from "templates/chat_status_modal.html";
 import tpl_profile_modal from "templates/profile_modal.html";
 import tpl_profile_view from "templates/profile_view.html";

+ 4 - 5
src/converse-rosterview.js

@@ -680,15 +680,14 @@ converse.plugins.add('converse-rosterview', {
                 this.filterOutContacts(this.getFilterMatches(q, type));
             },
 
-            toggle (ev) {
+            async toggle (ev) {
                 if (ev && ev.preventDefault) { ev.preventDefault(); }
                 const icon_el = ev.target.querySelector('.fa');
                 if (_.includes(icon_el.classList, "fa-caret-down")) {
                     this.model.save({state: _converse.CLOSED});
-                    this.collapse().then(() => {
-                        icon_el.classList.remove("fa-caret-down");
-                        icon_el.classList.add("fa-caret-right");
-                    });
+                    await this.collapse();
+                    icon_el.classList.remove("fa-caret-down");
+                    icon_el.classList.add("fa-caret-right");
                 } else {
                     icon_el.classList.remove("fa-caret-right");
                     icon_el.classList.add("fa-caret-down");

+ 18 - 16
src/headless/converse-chatboxes.js

@@ -144,27 +144,29 @@ converse.plugins.add('converse-chatboxes', {
                 return _converse.api.sendIQ(iq);
             },
 
-            getRequestSlotURL () {
-                this.sendSlotRequestStanza().then((stanza) => {
-                    const slot = stanza.querySelector('slot');
-                    if (slot) {
-                        this.save({
-                            'get':  slot.querySelector('get').getAttribute('url'),
-                            'put': slot.querySelector('put').getAttribute('url'),
-                        });
-                    } else {
-                        return this.save({
-                            'type': 'error',
-                            'message': __("Sorry, could not determine file upload URL.")
-                        });
-                    }
-                }).catch((e) => {
+            async getRequestSlotURL () {
+                let stanza;
+                try {
+                    stanza = await this.sendSlotRequestStanza();
+                } catch (e) {
                     _converse.log(e, Strophe.LogLevel.ERROR);
                     return this.save({
                         'type': 'error',
                         'message': __("Sorry, could not determine upload URL.")
                     });
-                });
+                }
+                const slot = stanza.querySelector('slot');
+                if (slot) {
+                    this.save({
+                        'get':  slot.querySelector('get').getAttribute('url'),
+                        'put': slot.querySelector('put').getAttribute('url'),
+                    });
+                } else {
+                    return this.save({
+                        'type': 'error',
+                        'message': __("Sorry, could not determine file upload URL.")
+                    });
+                }
             },
 
             uploadFile () {

+ 53 - 60
src/headless/converse-disco.js

@@ -162,13 +162,14 @@ converse.plugins.add('converse-disco', {
                 });
             },
 
-            queryForItems () {
+            async queryForItems () {
                 if (_.isEmpty(this.identities.where({'category': 'server'}))) {
                     // Don't fetch features and items if this is not a
                     // server or a conference component.
                     return;
                 }
-                _converse.api.disco.items(this.get('jid')).then(stanza => this.onDiscoItems(stanza));
+                const stanza = await _converse.api.disco.items(this.get('jid'));
+                this.onDiscoItems(stanza);
             },
 
             onInfo (stanza) {
@@ -268,7 +269,7 @@ converse.plugins.add('converse-disco', {
             _converse.emit('streamFeaturesAdded');
         }
 
-        function initializeDisco () {
+        async function initializeDisco () {
             addClientFeatures();
             _converse.connection.addHandler(onDiscoInfoRequest, Strophe.NS.DISCO_INFO, 'iq', 'get', null, null);
 
@@ -277,14 +278,13 @@ converse.plugins.add('converse-disco', {
                 b64_sha1(`converse.disco-entities-${_converse.bare_jid}`)
             );
 
-            _converse.disco_entities.fetchEntities().then((collection) => {
-                if (collection.length === 0 || !collection.get(_converse.domain)) {
-                    // If we don't have an entity for our own XMPP server,
-                    // create one.
-                    _converse.disco_entities.create({'jid': _converse.domain});
-                }
-                _converse.emit('discoInitialized');
-            }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+            const collection = await _converse.disco_entities.fetchEntities();
+            if (collection.length === 0 || !collection.get(_converse.domain)) {
+                // If we don't have an entity for our own XMPP server,
+                // create one.
+                _converse.disco_entities.create({'jid': _converse.domain});
+            }
+            _converse.emit('discoInitialized');
         }
 
         _converse.api.listen.on('sessionInitialized', initStreamFeatures);
@@ -513,18 +513,16 @@ converse.plugins.add('converse-disco', {
                      * @param {boolean} [create] Whether the entity should be created if it doesn't exist.
                      * @example _converse.api.disco.entities.get(jid);
                      */
-                    'get' (jid, create=false) {
-                        return _converse.api.waitUntil('discoInitialized')
-                        .then(() => {
-                            if (_.isNil(jid)) {
-                                return _converse.disco_entities;
-                            }
-                            const entity = _converse.disco_entities.get(jid);
-                            if (entity || !create) {
-                                return entity;
-                            }
-                            return _converse.disco_entities.create({'jid': jid});
-                        });
+                    async 'get' (jid, create=false) {
+                        await _converse.api.waitUntil('discoInitialized');
+                        if (_.isNil(jid)) {
+                            return _converse.disco_entities;
+                        }
+                        const entity = _converse.disco_entities.get(jid);
+                        if (entity || !create) {
+                            return entity;
+                        }
+                        return _converse.disco_entities.create({'jid': jid});
                     }
                 },
 
@@ -559,20 +557,19 @@ converse.plugins.add('converse-disco', {
                  *     );
                  * });
                  */
-                'supports' (feature, jid) {
+                async 'supports' (feature, jid) {
                     if (_.isNil(jid)) {
                         throw new TypeError('api.disco.supports: You need to provide an entity JID');
                     }
-                    return _converse.api.waitUntil('discoInitialized')
-                    .then(() => _converse.api.disco.entities.get(jid, true))
-                    .then(entity => entity.waitUntilFeaturesDiscovered)
-                    .then(entity => {
-                        const promises = _.concat(
-                            entity.items.map(item => item.hasFeature(feature)),
-                            entity.hasFeature(feature)
-                        );
-                        return Promise.all(promises);
-                    }).then(result => f.filter(f.isObject, result));
+                    await _converse.api.waitUntil('discoInitialized');
+                    let entity = await _converse.api.disco.entities.get(jid, true);
+                    entity = await entity.waitUntilFeaturesDiscovered;
+                    const promises = _.concat(
+                        entity.items.map(item => item.hasFeature(feature)),
+                        entity.hasFeature(feature)
+                    );
+                    const result = await Promise.all(promises);
+                    return f.filter(f.isObject, result);
                 },
 
                 /**
@@ -585,21 +582,18 @@ converse.plugins.add('converse-disco', {
                  * @example
                  * await _converse.api.disco.refreshFeatures('room@conference.example.org');
                  */
-                'refreshFeatures' (jid) {
+                async 'refreshFeatures' (jid) {
                     if (_.isNil(jid)) {
                         throw new TypeError('api.disco.refreshFeatures: You need to provide an entity JID');
                     }
-                    return _converse.api.waitUntil('discoInitialized')
-                        .then(() => _converse.api.disco.entities.get(jid, true))
-                        .then(entity => {
-                            entity.features.reset();
-                            entity.fields.reset();
-                            entity.identities.reset();
-                            entity.waitUntilFeaturesDiscovered = utils.getResolveablePromise()
-                            entity.queryInfo();
-                            return entity.waitUntilFeaturesDiscovered;
-                        })
-                        .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+                    await _converse.api.waitUntil('discoInitialized');
+                    const entity = await _converse.api.disco.entities.get(jid, true);
+                    entity.features.reset();
+                    entity.fields.reset();
+                    entity.identities.reset();
+                    entity.waitUntilFeaturesDiscovered = utils.getResolveablePromise()
+                    entity.queryInfo();
+                    return entity.waitUntilFeaturesDiscovered;
                 },
 
                 /**
@@ -611,15 +605,14 @@ converse.plugins.add('converse-disco', {
                  * @example
                  * const features = await _converse.api.disco.getFeatures('room@conference.example.org');
                  */
-                'getFeatures' (jid) {
+                async 'getFeatures' (jid) {
                     if (_.isNil(jid)) {
                         throw new TypeError('api.disco.getFeatures: You need to provide an entity JID');
                     }
-                    return _converse.api.waitUntil('discoInitialized')
-                    .then(() => _converse.api.disco.entities.get(jid, true))
-                    .then(entity => entity.waitUntilFeaturesDiscovered)
-                    .then(entity => entity.features)
-                    .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+                    await _converse.api.waitUntil('discoInitialized');
+                    let entity = await _converse.api.disco.entities.get(jid, true);
+                    entity = await entity.waitUntilFeaturesDiscovered;
+                    return entity.features;
                 },
 
                 /**
@@ -633,15 +626,14 @@ converse.plugins.add('converse-disco', {
                  * @example
                  * const fields = await _converse.api.disco.getFields('room@conference.example.org');
                  */
-                'getFields' (jid) {
+                async 'getFields' (jid) {
                     if (_.isNil(jid)) {
                         throw new TypeError('api.disco.getFields: You need to provide an entity JID');
                     }
-                    return _converse.api.waitUntil('discoInitialized')
-                    .then(() => _converse.api.disco.entities.get(jid, true))
-                    .then(entity => entity.waitUntilFeaturesDiscovered)
-                    .then(entity => entity.fields)
-                    .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+                    await _converse.api.waitUntil('discoInitialized');
+                    let entity = await _converse.api.disco.entities.get(jid, true);
+                    entity = await entity.waitUntilFeaturesDiscovered;
+                    return entity.fields;
                 },
 
                 /**
@@ -675,8 +667,9 @@ converse.plugins.add('converse-disco', {
                  *     }
                  * ).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
                  */
-                'getIdentity' (category, type, jid) {
-                    return _converse.api.disco.entities.get(jid, true).then(e => e.getIdentity(category, type));
+                async 'getIdentity' (category, type, jid) {
+                    const e = await _converse.api.disco.entities.get(jid, true);
+                    return e.getIdentity(category, type);
                 }
             }
         });

+ 46 - 52
src/headless/converse-muc.js

@@ -109,20 +109,18 @@ converse.plugins.add('converse-muc', {
         _converse.api.promises.add(['roomsAutoJoined']);
 
 
-        function openRoom (jid) {
+        async function openRoom (jid) {
             if (!u.isValidMUCJID(jid)) {
                 return _converse.log(
                     `Invalid JID "${jid}" provided in URL fragment`,
                     Strophe.LogLevel.WARN
                 );
             }
-            const promises = [_converse.api.waitUntil('roomsAutoJoined')]
+            await _converse.api.waitUntil('roomsAutoJoined');
             if (_converse.allow_bookmarks) {
-                promises.push( _converse.api.waitUntil('bookmarksInitialized'));
+                await _converse.api.waitUntil('bookmarksInitialized');
             }
-            Promise.all(promises).then(() => {
-                _converse.api.rooms.open(jid);
-            });
+            _converse.api.rooms.open(jid);
         }
         _converse.router.route('converse/room?jid=:jid', openRoom);
 
@@ -762,7 +760,7 @@ converse.plugins.add('converse-muc', {
                 }
             },
 
-            checkForReservedNick () {
+            async checkForReservedNick () {
                 /* Use service-discovery to ask the XMPP server whether
                  * this user has a reserved nickname for this groupchat.
                  * If so, we'll use that, otherwise we render the nickname form.
@@ -771,7 +769,7 @@ converse.plugins.add('converse-muc', {
                  *  (Function) callback: Callback upon succesful IQ response
                  *  (Function) errback: Callback upon error IQ response
                  */
-                return _converse.api.sendIQ(
+                const iq = await _converse.api.sendIQ(
                     $iq({
                         'to': this.get('jid'),
                         'from': _converse.connection.jid,
@@ -780,15 +778,14 @@ converse.plugins.add('converse-muc', {
                         'xmlns': Strophe.NS.DISCO_INFO,
                         'node': 'x-roomuser-item'
                     })
-                ).then(iq => {
-                    const identity_el = iq.querySelector('query[node="x-roomuser-item"] identity'),
-                          nick = identity_el ? identity_el.getAttribute('name') : null;
-                    this.save({
-                        'reserved_nick': nick,
-                        'nick': nick
-                    }, {'silent': true});
-                    return iq;
-                });
+                );
+                const identity_el = iq.querySelector('query[node="x-roomuser-item"] identity'),
+                      nick = identity_el ? identity_el.getAttribute('name') : null;
+                this.save({
+                    'reserved_nick': nick,
+                    'nick': nick
+                }, {'silent': true});
+                return iq;
             },
 
             async registerNickname () {
@@ -939,7 +936,7 @@ converse.plugins.add('converse-muc', {
                 }
             },
 
-            onMessage (stanza) {
+            async onMessage (stanza) {
                 /* Handler for all MUC messages sent to this groupchat.
                  *
                  * Parameters:
@@ -968,9 +965,8 @@ converse.plugins.add('converse-muc', {
                         const subject = _.propertyOf(subject_el)('textContent') || '';
                         u.safeSave(this, {'subject': {'author': sender, 'text': subject}});
                     }
-                    this.createMessage(stanza, original_stanza)
-                        .then(msg => this.incrementUnreadMsgCounter(msg))
-                        .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+                    const msg = await this.createMessage(stanza, original_stanza);
+                    this.incrementUnreadMsgCounter(msg);
                 }
                 if (sender !== this.get('nick')) {
                     // We only emit an event if it's not our own message
@@ -1141,37 +1137,35 @@ converse.plugins.add('converse-muc', {
                 }
             },
 
-            fetchMembers () {
-                this.chatroom.getJidsWithAffiliations(['member', 'owner', 'admin'])
-                .then(new_members => {
-                    const new_jids = new_members.map(m => m.jid).filter(m => !_.isUndefined(m)),
-                          new_nicks = new_members.map(m => !m.jid && m.nick || undefined).filter(m => !_.isUndefined(m)),
-                          removed_members = this.filter(m => {
-                              return f.includes(m.get('affiliation'), ['admin', 'member', 'owner']) &&
-                                  !f.includes(m.get('nick'), new_nicks) &&
-                                    !f.includes(m.get('jid'), new_jids);
-                          });
-
-                    _.each(removed_members, (occupant) => {
-                        if (occupant.get('jid') === _converse.bare_jid) { return; }
-                        if (occupant.get('show') === 'offline') {
-                            occupant.destroy();
-                        }
-                    });
-                    _.each(new_members, (attrs) => {
-                        let occupant;
-                        if (attrs.jid) {
-                            occupant = this.findOccupant({'jid': attrs.jid});
-                        } else {
-                            occupant = this.findOccupant({'nick': attrs.nick});
-                        }
-                        if (occupant) {
-                            occupant.save(attrs);
-                        } else {
-                            this.create(attrs);
-                        }
-                    });
-                }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
+            async fetchMembers () {
+                const new_members = await this.chatroom.getJidsWithAffiliations(['member', 'owner', 'admin']);
+                const new_jids = new_members.map(m => m.jid).filter(m => !_.isUndefined(m)),
+                      new_nicks = new_members.map(m => !m.jid && m.nick || undefined).filter(m => !_.isUndefined(m)),
+                      removed_members = this.filter(m => {
+                          return f.includes(m.get('affiliation'), ['admin', 'member', 'owner']) &&
+                              !f.includes(m.get('nick'), new_nicks) &&
+                                !f.includes(m.get('jid'), new_jids);
+                      });
+
+                _.each(removed_members, (occupant) => {
+                    if (occupant.get('jid') === _converse.bare_jid) { return; }
+                    if (occupant.get('show') === 'offline') {
+                        occupant.destroy();
+                    }
+                });
+                _.each(new_members, (attrs) => {
+                    let occupant;
+                    if (attrs.jid) {
+                        occupant = this.findOccupant({'jid': attrs.jid});
+                    } else {
+                        occupant = this.findOccupant({'nick': attrs.nick});
+                    }
+                    if (occupant) {
+                        occupant.save(attrs);
+                    } else {
+                        this.create(attrs);
+                    }
+                });
             },
 
             findOccupant (data) {

+ 0 - 8
src/headless/utils/core.js

@@ -333,14 +333,6 @@ u.replaceCurrentWord = function (input, new_value) {
     input.selectionEnd = cursor - current_word.length + new_value.length + 1;
 };
 
-u.isVisible = function (el) {
-    if (u.hasClass('hidden', el)) {
-        return false;
-    }
-    // XXX: Taken from jQuery's "visible" implementation
-    return el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0;
-};
-
 u.triggerEvent = function (el, name, type="Event", bubbles=true, cancelable=true) {
     const evt = document.createEvent(type);
     evt.initEvent(name, bubbles, cancelable);

+ 1 - 1
src/templates/alert.html

@@ -1 +1 @@
-<div class="alert {{{o.type}}}" role="alert">{{{o.message}}}</div>
+<div class="alert {{{o.type}}}" role="alert"><p>{{{o.message}}}</p></div>

+ 9 - 0
src/utils/html.js

@@ -500,6 +500,15 @@ function afterAnimationEnds (el, callback) {
     }
 }
 
+u.isVisible = function (el) {
+    if (u.hasClass('hidden', el)) {
+        return false;
+    }
+    // XXX: Taken from jQuery's "visible" implementation
+    return el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0;
+};
+
+
 u.fadeIn = function (el, callback) {
     if (_.isNil(el)) {
         logger.warn("Undefined or null element passed into fadeIn");

+ 0 - 1
tests/runner.js

@@ -179,7 +179,6 @@ require.config(config);
 var specs = [
     "jasmine",
     //"spec/transcripts",
-    //"spec/otr",
     "spec/spoilers",
     "spec/profiling",
     "spec/utils",

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно