Преглед изворни кода

Use `_coverse.api.sendIQ` instead of `_converse.connection.sendIQ`

So that we know an event will be emitted when the stanza is sent.
JC Brand пре 6 година
родитељ
комит
c583678c86

+ 108 - 119
dist/converse.js

@@ -63028,13 +63028,15 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].plugins
       updateRoomsList() {
         /* Send an IQ stanza to the server asking for all groupchats
          */
-        _converse.connection.sendIQ($iq({
+        const iq = $iq({
           'to': this.model.get('muc_domain'),
           'from': _converse.connection.jid,
           'type': "get"
         }).c("query", {
           xmlns: Strophe.NS.DISCO_ITEMS
-        }), this.onRoomsFound.bind(this), this.informNoRoomsFound.bind(this), 5000);
+        });
+
+        _converse.api.sendIQ(iq).then(iq => this.onRoomsFound(iq)).catch(iq => this.informNoRoomsFound());
       },
 
       showRooms(ev) {
@@ -63512,7 +63514,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].plugins
           iq.c("reason", reason);
         }
 
-        return _converse.connection.sendIQ(iq, onSuccess, onError);
+        return _converse.api.sendIQ(iq).then(onSuccess).catch(onError);
       },
 
       verifyRoles(roles) {
@@ -73262,9 +73264,9 @@ _converse.api = {
    * @returns {Promise} A promise which resolves when we receive a `result` stanza
    * or is rejected when we receive an `error` stanza.
    */
-  'sendIQ'(stanza) {
+  'sendIQ'(stanza, timeout) {
     return new es6_promise_dist_es6_promise_auto__WEBPACK_IMPORTED_MODULE_2___default.a((resolve, reject) => {
-      _converse.connection.sendIQ(stanza, resolve, reject, _converse.IQ_TIMEOUT);
+      _converse.connection.sendIQ(stanza, resolve, reject, timeout || _converse.IQ_TIMEOUT);
 
       _converse.emit('send', stanza);
     });
@@ -74295,7 +74297,7 @@ function queryForArchivedMessages(_converse, options, callback, errback) {
     return true;
   }, Strophe.NS.MAM);
 
-  _converse.connection.sendIQ(stanza, function (iq) {
+  _converse.api.sendIQ(stanza, _converse.message_archiving_timeout).then(iq => {
     _converse.connection.deleteHandler(message_handler);
 
     if (_.isFunction(callback)) {
@@ -74312,13 +74314,15 @@ function queryForArchivedMessages(_converse, options, callback, errback) {
 
       callback(messages, rsm);
     }
-  }, function () {
+  }).catch(e => {
     _converse.connection.deleteHandler(message_handler);
 
     if (_.isFunction(errback)) {
       errback.apply(this, arguments);
     }
-  }, _converse.message_archiving_timeout);
+
+    return;
+  });
 }
 
 _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam', {
@@ -74556,7 +74560,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam
 
     });
 
-    _converse.onMAMError = function (model, iq) {
+    _converse.onMAMError = function (iq) {
       if (iq.querySelectorAll('feature-not-implemented').length) {
         _converse.log("Message Archive Management (XEP-0313) not supported by this server", Strophe.LogLevel.WARN);
       } else {
@@ -74588,20 +74592,16 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam
           'default': _converse.message_archiving
         });
 
-        _.each(preference.children, function (child) {
-          stanza.cnode(child).up();
-        });
+        _.each(preference.children, child => stanza.cnode(child).up()); // XXX: Strictly speaking, the server should respond with the updated prefs
+        // (see example 18: https://xmpp.org/extensions/xep-0313.html#config)
+        // but Prosody doesn't do this, so we don't rely on it.
 
-        _converse.connection.sendIQ(stanza, _.partial(function (feature, iq) {
-          // XXX: Strictly speaking, the server should respond with the updated prefs
-          // (see example 18: https://xmpp.org/extensions/xep-0313.html#config)
-          // but Prosody doesn't do this, so we don't rely on it.
-          feature.save({
-            'preferences': {
-              'default': _converse.message_archiving
-            }
-          });
-        }, feature), _converse.onMAMError);
+
+        _converse.api.sendIQ(stanza).then(() => feature.save({
+          'preferences': {
+            'default': _converse.message_archiving
+          }
+        })).catch(_converse.onMAMError);
       } else {
         feature.save({
           'preferences': {
@@ -74619,11 +74619,11 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam
       if (feature.get('var') === Strophe.NS.MAM && prefs['default'] !== _converse.message_archiving && // eslint-disable-line dot-notation
       !_.isUndefined(_converse.message_archiving)) {
         // Ask the server for archiving preferences
-        _converse.connection.sendIQ($iq({
+        _converse.api.sendIQ($iq({
           'type': 'get'
         }).c('prefs', {
           'xmlns': Strophe.NS.MAM
-        }), _.partial(_converse.onMAMPreferences, feature), _.partial(_converse.onMAMError, feature));
+        })).then(_.partial(_converse.onMAMPreferences, feature)).catch(_converse.onMAMError);
       }
     });
 
@@ -75552,14 +75552,12 @@ _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].plugins.add('converse-muc
          * Returns a promise which resolves once the response IQ
          * has been received.
          */
-        return new Promise((resolve, reject) => {
-          _converse.connection.sendIQ($iq({
-            'to': this.get('jid'),
-            'type': "get"
-          }).c("query", {
-            xmlns: Strophe.NS.MUC_OWNER
-          }), resolve, reject);
-        });
+        return _converse.api.sendIQ($iq({
+          'to': this.get('jid'),
+          'type': "get"
+        }).c("query", {
+          xmlns: Strophe.NS.MUC_OWNER
+        }));
       },
 
       sendConfiguration(config, callback, errback) {
@@ -75592,7 +75590,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].plugins.add('converse-muc
 
         callback = _.isUndefined(callback) ? _.noop : _.partial(callback, iq.nodeTree);
         errback = _.isUndefined(errback) ? _.noop : _.partial(errback, iq.nodeTree);
-        return _converse.connection.sendIQ(iq, callback, errback);
+        return _converse.api.sendIQ(iq).then(callback).catch(errback);
       },
 
       saveAffiliationAndRole(pres) {
@@ -75632,24 +75630,22 @@ _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].plugins.add('converse-muc
          *  (Object) member: Map containing the member's jid and
          *      optionally a reason and affiliation.
          */
-        return new Promise((resolve, reject) => {
-          const iq = $iq({
-            to: this.get('jid'),
-            type: "set"
-          }).c("query", {
-            xmlns: Strophe.NS.MUC_ADMIN
-          }).c("item", {
-            'affiliation': member.affiliation || affiliation,
-            'nick': member.nick,
-            'jid': member.jid
-          });
+        const iq = $iq({
+          to: this.get('jid'),
+          type: "set"
+        }).c("query", {
+          xmlns: Strophe.NS.MUC_ADMIN
+        }).c("item", {
+          'affiliation': member.affiliation || affiliation,
+          'nick': member.nick,
+          'jid': member.jid
+        });
 
-          if (!_.isUndefined(member.reason)) {
-            iq.c("reason", member.reason);
-          }
+        if (!_.isUndefined(member.reason)) {
+          iq.c("reason", member.reason);
+        }
 
-          _converse.connection.sendIQ(iq, resolve, reject);
-        });
+        return _converse.api.sendIQ(iq);
       },
 
       setAffiliations(members) {
@@ -77073,7 +77069,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
           subscription: "remove"
         });
 
-        _converse.connection.sendIQ(iq, callback, errback);
+        _converse.api.sendIQ(iq).then(callback).catch(errback);
 
         return this;
       }
@@ -77197,7 +77193,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
         this.addContactToRoster(jid, name, groups, attributes).then(handler, handler);
       },
 
-      sendContactAddIQ(jid, name, groups, callback, errback) {
+      sendContactAddIQ(jid, name, groups) {
         /*  Send an IQ stanza to the XMPP server to add a new roster contact.
          *
          *  Parameters:
@@ -77209,22 +77205,20 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
          */
         name = _.isEmpty(name) ? jid : name;
         const iq = $iq({
-          type: 'set'
+          'type': 'set'
         }).c('query', {
-          xmlns: Strophe.NS.ROSTER
+          'xmlns': Strophe.NS.ROSTER
         }).c('item', {
           jid,
           name
         });
 
-        _.each(groups, function (group) {
-          iq.c('group').t(group).up();
-        });
+        _.each(groups, group => iq.c('group').t(group).up());
 
-        _converse.connection.sendIQ(iq, callback, errback);
+        _converse.api.sendIQ(iq);
       },
 
-      addContactToRoster(jid, name, groups, attributes) {
+      async addContactToRoster(jid, name, groups, attributes) {
         /* Adds a RosterContact instance to _converse.roster and
          * registers the contact on the XMPP server.
          * Returns a promise which is resolved once the XMPP server has
@@ -77236,27 +77230,26 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
          *    (Array of Strings) groups - Any roster groups the user might belong to
          *    (Object) attributes - Any additional attributes to be stored on the user's model.
          */
-        return new Promise((resolve, reject) => {
-          groups = groups || [];
-          this.sendContactAddIQ(jid, name, groups, () => {
-            const contact = this.create(_.assignIn({
-              'ask': undefined,
-              'nickname': name,
-              groups,
-              jid,
-              'requesting': false,
-              'subscription': 'none'
-            }, attributes), {
-              sort: false
-            });
-            resolve(contact);
-          }, function (err) {
-            alert(__('Sorry, there was an error while trying to add %1$s as a contact.', name));
+        groups = groups || [];
 
-            _converse.log(err, Strophe.LogLevel.ERROR);
+        try {
+          await this.sendContactAddIQ(jid, name, groups);
+        } catch (e) {
+          _converse.log(e, Strophe.LogLevel.ERROR);
 
-            resolve(err);
-          });
+          alert(__('Sorry, there was an error while trying to add %1$s as a contact.', name));
+          return e;
+        }
+
+        return this.create(_.assignIn({
+          'ask': undefined,
+          'nickname': name,
+          groups,
+          jid,
+          'requesting': false,
+          'subscription': 'none'
+        }, attributes), {
+          'sort': false
         });
       },
 
@@ -77346,34 +77339,32 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
         return _converse.api.disco.stream.getFeature('ver', 'urn:xmpp:features:rosterver') && this.data.get('version');
       },
 
-      fetchFromServer() {
+      async fetchFromServer() {
         /* Fetch the roster from the XMPP server */
-        return new Promise((resolve, reject) => {
-          const iq = $iq({
-            'type': 'get',
-            'id': _converse.connection.getUniqueId('roster')
-          }).c('query', {
-            xmlns: Strophe.NS.ROSTER
-          });
-
-          if (this.rosterVersioningSupported()) {
-            iq.attrs({
-              'ver': this.data.get('version')
-            });
-          }
+        const stanza = $iq({
+          'type': 'get',
+          'id': _converse.connection.getUniqueId('roster')
+        }).c('query', {
+          xmlns: Strophe.NS.ROSTER
+        });
 
-          const callback = _.flow(this.onReceivedFromServer.bind(this), resolve);
+        if (this.rosterVersioningSupported()) {
+          stanza.attrs({
+            'ver': this.data.get('version')
+          });
+        }
 
-          const errback = function errback(iq) {
-            const errmsg = "Error while trying to fetch roster from the server";
+        let iq;
 
-            _converse.log(errmsg, Strophe.LogLevel.ERROR);
+        try {
+          iq = await _converse.api.sendIQ(stanza);
+        } catch (e) {
+          _converse.log(e, Strophe.LogLevel.ERROR);
 
-            reject(new Error(errmsg));
-          };
+          return _converse.log("Error while trying to fetch roster from the server", Strophe.LogLevel.ERROR);
+        }
 
-          return _converse.connection.sendIQ(iq, callback, errback);
-        });
+        return this.onReceivedFromServer(iq);
       },
 
       onReceivedFromServer(iq) {
@@ -77834,7 +77825,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins.add('converse-vca
 
     });
 
-    function onVCardData(jid, iq, callback) {
+    async function onVCardData(jid, iq) {
       const vcard = iq.querySelector('vCard');
       let result = {};
 
@@ -77855,23 +77846,11 @@ _converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins.add('converse-vca
 
       if (result.image) {
         const buffer = u.base64ToArrayBuffer(result['image']);
-        crypto.subtle.digest('SHA-1', buffer).then(ab => {
-          result['image_hash'] = u.arrayBufferToHex(ab);
-          if (callback) callback(result);
-        });
-      } else {
-        if (callback) callback(result);
+        const ab = await crypto.subtle.digest('SHA-1', buffer);
+        result['image_hash'] = u.arrayBufferToHex(ab);
       }
-    }
 
-    function onVCardError(jid, iq, errback) {
-      if (errback) {
-        errback({
-          'stanza': iq,
-          'jid': jid,
-          'vcard_error': moment().format()
-        });
-      }
+      return result;
     }
 
     function createStanza(type, jid, vcard_el) {
@@ -77902,7 +77881,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins.add('converse-vca
       return _converse.api.sendIQ(createStanza("set", jid, vcard_el));
     }
 
-    function getVCard(_converse, jid) {
+    async function getVCard(_converse, jid) {
       /* Request the VCard of another user. Returns a promise.
        *
        * Parameters:
@@ -77910,9 +77889,19 @@ _converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins.add('converse-vca
        *      is being requested.
        */
       const to = Strophe.getBareJidFromJid(jid) === _converse.bare_jid ? null : jid;
-      return new Promise((resolve, reject) => {
-        _converse.connection.sendIQ(createStanza("get", to), _.partial(onVCardData, jid, _, resolve), _.partial(onVCardError, jid, _, resolve), _converse.IQ_TIMEOUT);
-      });
+      let iq;
+
+      try {
+        iq = await _converse.api.sendIQ(createStanza("get", to));
+      } catch (iq) {
+        return {
+          'stanza': iq,
+          'jid': jid,
+          'vcard_error': moment().format()
+        };
+      }
+
+      return onVCardData(jid, iq);
     }
     /* Event handlers */
 

+ 45 - 49
spec/chatroom.js

@@ -3737,9 +3737,9 @@
             }));
 
             it("contains a link to a modal which can list groupchats publically available on the server",
-                    mock.initConverseWithPromises(
-                        null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                        function (done, _converse) {
+                mock.initConverseWithPromises(
+                    null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                    async function (done, _converse) {
 
                 var sendIQ = _converse.connection.sendIQ;
                 var sent_stanza, IQ_id;
@@ -3753,53 +3753,49 @@
                 roomspanel.el.querySelector('.show-list-muc-modal').click();
                 test_utils.closeControlBox(_converse);
                 const modal = roomspanel.list_rooms_modal;
-                test_utils.waitUntil(() => u.isVisible(modal.el), 1000)
-                .then(() => {
-                    spyOn(_converse.ChatRoom.prototype, 'getRoomFeatures').and.callFake(() => Promise.resolve());
-                    roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
-
-                    // See: http://xmpp.org/extensions/xep-0045.html#disco-rooms
-                    expect(modal.el.querySelectorAll('.available-chatrooms li').length).toBe(0);
-
-                    const input = modal.el.querySelector('input[name="server"]').value = 'chat.shakespear.lit';
-                    modal.el.querySelector('input[type="submit"]').click();
-                    return test_utils.waitUntil(() => _converse.chatboxes.length);
-                }).then(() => {
-                    expect(sent_stanza.toLocaleString()).toBe(
-                        `<iq from="dummy@localhost/resource" id="${IQ_id}" to="chat.shakespear.lit" type="get" xmlns="jabber:client">`+
-                            `<query xmlns="http://jabber.org/protocol/disco#items"/>`+
-                        `</iq>`
-                    );
+                await test_utils.waitUntil(() => u.isVisible(modal.el), 1000);
+                spyOn(_converse.ChatRoom.prototype, 'getRoomFeatures').and.callFake(() => Promise.resolve());
+                roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
+
+                // See: http://xmpp.org/extensions/xep-0045.html#disco-rooms
+                expect(modal.el.querySelectorAll('.available-chatrooms li').length).toBe(0);
+
+                const input = modal.el.querySelector('input[name="server"]').value = 'chat.shakespear.lit';
+                modal.el.querySelector('input[type="submit"]').click();
+                await test_utils.waitUntil(() => _converse.chatboxes.length);
+                expect(sent_stanza.toLocaleString()).toBe(
+                    `<iq from="dummy@localhost/resource" id="${IQ_id}" to="chat.shakespear.lit" type="get" xmlns="jabber:client">`+
+                        `<query xmlns="http://jabber.org/protocol/disco#items"/>`+
+                    `</iq>`
+                );
 
-                    var iq = $iq({
-                        from:'muc.localhost',
-                        to:'dummy@localhost/pda',
-                        id: IQ_id,
-                        type:'result'
-                    }).c('query')
-                    .c('item', { jid:'heath@chat.shakespeare.lit', name:'A Lonely Heath'}).up()
-                    .c('item', { jid:'coven@chat.shakespeare.lit', name:'A Dark Cave'}).up()
-                    .c('item', { jid:'forres@chat.shakespeare.lit', name:'The Palace'}).up()
-                    .c('item', { jid:'inverness@chat.shakespeare.lit', name:'Macbeth&apos;s Castle'}).nodeTree;
-                    _converse.connection._dataRecv(test_utils.createRequest(iq));
-
-                    expect(modal.el.querySelectorAll('.available-chatrooms li').length).toBe(5);
-
-                    const rooms = modal.el.querySelectorAll('.available-chatrooms li');
-                    expect(rooms[0].textContent.trim()).toBe("Groupchats found:");
-                    expect(rooms[1].textContent.trim()).toBe("A Lonely Heath");
-                    expect(rooms[2].textContent.trim()).toBe("A Dark Cave");
-                    expect(rooms[3].textContent.trim()).toBe("The Palace");
-                    expect(rooms[4].textContent.trim()).toBe("Macbeth's Castle");
-
-                    rooms[4].querySelector('.open-room').click();
-                    return test_utils.waitUntil(() => _converse.chatboxes.length > 1);
-                }).then(() => {
-                    expect(sizzle('.chatroom', _converse.el).filter(u.isVisible).length).toBe(1); // There should now be an open chatroom
-                    var view = _converse.chatboxviews.get('inverness@chat.shakespeare.lit');
-                    expect(view.el.querySelector('.chat-head-chatroom').textContent.trim()).toBe("Macbeth's Castle");
-                    done();
-                }).catch(_.partial(console.error, _));
+                const iq = $iq({
+                    from:'muc.localhost',
+                    to:'dummy@localhost/pda',
+                    id: IQ_id,
+                    type:'result'
+                }).c('query')
+                .c('item', { jid:'heath@chat.shakespeare.lit', name:'A Lonely Heath'}).up()
+                .c('item', { jid:'coven@chat.shakespeare.lit', name:'A Dark Cave'}).up()
+                .c('item', { jid:'forres@chat.shakespeare.lit', name:'The Palace'}).up()
+                .c('item', { jid:'inverness@chat.shakespeare.lit', name:'Macbeth&apos;s Castle'}).nodeTree;
+                _converse.connection._dataRecv(test_utils.createRequest(iq));
+
+                await test_utils.waitUntil(() => modal.el.querySelectorAll('.available-chatrooms li').length === 5);
+
+                const rooms = modal.el.querySelectorAll('.available-chatrooms li');
+                expect(rooms[0].textContent.trim()).toBe("Groupchats found:");
+                expect(rooms[1].textContent.trim()).toBe("A Lonely Heath");
+                expect(rooms[2].textContent.trim()).toBe("A Dark Cave");
+                expect(rooms[3].textContent.trim()).toBe("The Palace");
+                expect(rooms[4].textContent.trim()).toBe("Macbeth's Castle");
+
+                rooms[4].querySelector('.open-room').click();
+                await test_utils.waitUntil(() => _converse.chatboxes.length > 1);
+                expect(sizzle('.chatroom', _converse.el).filter(u.isVisible).length).toBe(1); // There should now be an open chatroom
+                var view = _converse.chatboxviews.get('inverness@chat.shakespeare.lit');
+                expect(view.el.querySelector('.chat-head-chatroom').textContent.trim()).toBe("Macbeth's Castle");
+                done();
             }));
 
             it("shows the number of unread mentions received",

+ 239 - 239
spec/mam.js

@@ -157,79 +157,79 @@
             it("checks whether returned MAM messages from a MUC room are from the right JID",
                     mock.initConverseWithPromises(
                         null, [], {},
-                        function (done, _converse) {
-
-                _converse.api.disco.entities.get(_converse.domain).then(function (entity) {
-                    if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
-                        _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
-                    }
-                    var sent_stanza, IQ_id;
-                    var sendIQ = _converse.connection.sendIQ;
-                    spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
-                        sent_stanza = iq;
-                        IQ_id = sendIQ.bind(this)(iq, callback, errback);
-                    });
-                    var callback = jasmine.createSpy('callback');
+                        async function (done, _converse) {
 
-                    _converse.api.archive.query({'with': 'coven@chat.shakespear.lit', 'groupchat': true, 'max':'10'}, callback);
-                    var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
+                const entity = await _converse.api.disco.entities.get(_converse.domain);
+                if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
+                    _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
+                }
+                var sent_stanza, IQ_id;
+                const sendIQ = _converse.connection.sendIQ;
+                spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
+                    sent_stanza = iq;
+                    IQ_id = sendIQ.bind(this)(iq, callback, errback);
+                });
+                var callback = jasmine.createSpy('callback');
 
-                    /* <message id='iasd207' from='coven@chat.shakespeare.lit' to='hag66@shakespeare.lit/pda'>
-                     *     <result xmlns='urn:xmpp:mam:2' queryid='g27' id='34482-21985-73620'>
-                     *         <forwarded xmlns='urn:xmpp:forward:0'>
-                     *         <delay xmlns='urn:xmpp:delay' stamp='2002-10-13T23:58:37Z'/>
-                     *         <message xmlns="jabber:client"
-                     *             from='coven@chat.shakespeare.lit/firstwitch'
-                     *             id='162BEBB1-F6DB-4D9A-9BD8-CFDCC801A0B2'
-                     *             type='groupchat'>
-                     *             <body>Thrice the brinded cat hath mew'd.</body>
-                     *             <x xmlns='http://jabber.org/protocol/muc#user'>
-                     *             <item affiliation='none'
-                     *                     jid='witch1@shakespeare.lit'
-                     *                     role='participant' />
-                     *             </x>
-                     *         </message>
-                     *         </forwarded>
-                     *     </result>
-                     * </message>
-                     */
-                    var msg1 = $msg({'id':'iasd207', 'from': 'other@chat.shakespear.lit', 'to': 'dummy@localhost'})
-                                .c('result',  {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'34482-21985-73620'})
-                                    .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
-                                        .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
-                                        .c('message', {
-                                            'xmlns':'jabber:client',
-                                            'to':'dummy@localhost',
-                                            'id':'162BEBB1-F6DB-4D9A-9BD8-CFDCC801A0B2',
-                                            'from':'coven@chat.shakespeare.lit/firstwitch',
-                                            'type':'groupchat' })
-                                        .c('body').t("Thrice the brinded cat hath mew'd.");
-                    _converse.connection._dataRecv(test_utils.createRequest(msg1));
-
-                    /* Send an <iq> stanza to indicate the end of the result set.
-                     *
-                     * <iq type='result' id='juliet1'>
-                     *     <fin xmlns='urn:xmpp:mam:2'>
-                     *     <set xmlns='http://jabber.org/protocol/rsm'>
-                     *         <first index='0'>28482-98726-73623</first>
-                     *         <last>09af3-cc343-b409f</last>
-                     *         <count>20</count>
-                     *     </set>
-                     * </iq>
-                     */
-                    var stanza = $iq({'type': 'result', 'id': IQ_id})
-                        .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
-                            .c('set',  {'xmlns': 'http://jabber.org/protocol/rsm'})
-                                .c('first', {'index': '0'}).t('23452-4534-1').up()
-                                .c('last').t('09af3-cc343-b409f').up()
-                                .c('count').t('16');
-                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                _converse.api.archive.query({'with': 'coven@chat.shakespear.lit', 'groupchat': true, 'max':'10'}, callback);
+                var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
 
-                    expect(callback).toHaveBeenCalled();
-                    var args = callback.calls.argsFor(0);
-                    expect(args[0].length).toBe(0);
-                    done();
-                });
+                /* <message id='iasd207' from='coven@chat.shakespeare.lit' to='hag66@shakespeare.lit/pda'>
+                 *     <result xmlns='urn:xmpp:mam:2' queryid='g27' id='34482-21985-73620'>
+                 *         <forwarded xmlns='urn:xmpp:forward:0'>
+                 *         <delay xmlns='urn:xmpp:delay' stamp='2002-10-13T23:58:37Z'/>
+                 *         <message xmlns="jabber:client"
+                 *             from='coven@chat.shakespeare.lit/firstwitch'
+                 *             id='162BEBB1-F6DB-4D9A-9BD8-CFDCC801A0B2'
+                 *             type='groupchat'>
+                 *             <body>Thrice the brinded cat hath mew'd.</body>
+                 *             <x xmlns='http://jabber.org/protocol/muc#user'>
+                 *             <item affiliation='none'
+                 *                     jid='witch1@shakespeare.lit'
+                 *                     role='participant' />
+                 *             </x>
+                 *         </message>
+                 *         </forwarded>
+                 *     </result>
+                 * </message>
+                 */
+                var msg1 = $msg({'id':'iasd207', 'from': 'other@chat.shakespear.lit', 'to': 'dummy@localhost'})
+                            .c('result',  {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'34482-21985-73620'})
+                                .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
+                                    .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
+                                    .c('message', {
+                                        'xmlns':'jabber:client',
+                                        'to':'dummy@localhost',
+                                        'id':'162BEBB1-F6DB-4D9A-9BD8-CFDCC801A0B2',
+                                        'from':'coven@chat.shakespeare.lit/firstwitch',
+                                        'type':'groupchat' })
+                                    .c('body').t("Thrice the brinded cat hath mew'd.");
+                _converse.connection._dataRecv(test_utils.createRequest(msg1));
+
+                /* Send an <iq> stanza to indicate the end of the result set.
+                 *
+                 * <iq type='result' id='juliet1'>
+                 *     <fin xmlns='urn:xmpp:mam:2'>
+                 *     <set xmlns='http://jabber.org/protocol/rsm'>
+                 *         <first index='0'>28482-98726-73623</first>
+                 *         <last>09af3-cc343-b409f</last>
+                 *         <count>20</count>
+                 *     </set>
+                 * </iq>
+                 */
+                const stanza = $iq({'type': 'result', 'id': IQ_id})
+                    .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
+                        .c('set',  {'xmlns': 'http://jabber.org/protocol/rsm'})
+                            .c('first', {'index': '0'}).t('23452-4534-1').up()
+                            .c('last').t('09af3-cc343-b409f').up()
+                            .c('count').t('16');
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                await test_utils.waitUntil(() => callback.calls.count());
+                expect(callback).toHaveBeenCalled();
+                var args = callback.calls.argsFor(0);
+                expect(args[0].length).toBe(0);
+                done();
            }));
 
            it("can be used to query for all messages in a certain timespan",
@@ -277,19 +277,18 @@
            }));
 
            it("throws a TypeError if an invalid date is provided",
-                    mock.initConverseWithPromises(
-                        null, [], {},
-                        function (done, _converse) {
+                mock.initConverseWithPromises(
+                    null, [], {},
+                    async function (done, _converse) {
 
-                _converse.api.disco.entities.get(_converse.domain).then(function (entity) {
-                    if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
-                        _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
-                    }
-                    expect(_.partial(_converse.api.archive.query, {'start': 'not a real date'})).toThrow(
-                        new TypeError('archive.query: invalid date provided for: start')
-                    );
-                    done();
-                });
+                const entity = await _converse.api.disco.entities.get(_converse.domain);
+                if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
+                    _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
+                }
+                expect(_.partial(_converse.api.archive.query, {'start': 'not a real date'})).toThrow(
+                    new TypeError('archive.query: invalid date provided for: start')
+                );
+                done();
            }));
 
            it("can be used to query for all messages after a certain time",
@@ -498,175 +497,176 @@
            }));
 
            it("accepts a callback function, which it passes the messages and a Strophe.RSM object",
-                    mock.initConverseWithPromises(
-                        null, [], {},
-                        function (done, _converse) {
-
-                _converse.api.disco.entities.get(_converse.domain).then(function (entity) {
-                    if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
-                        _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
-                    }
-                    var sent_stanza, IQ_id;
-                    var sendIQ = _converse.connection.sendIQ;
-                    spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
-                        sent_stanza = iq;
-                        IQ_id = sendIQ.bind(this)(iq, callback, errback);
-                    });
-                    var callback = jasmine.createSpy('callback');
-
-                    _converse.api.archive.query({'with': 'romeo@capulet.lit', 'max':'10'}, callback);
-                    var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
-
-                    /*  <message id='aeb213' to='juliet@capulet.lit/chamber'>
-                     *  <result xmlns='urn:xmpp:mam:2' queryid='f27' id='28482-98726-73623'>
-                     *      <forwarded xmlns='urn:xmpp:forward:0'>
-                     *      <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>
-                     *      <message xmlns='jabber:client'
-                     *          to='juliet@capulet.lit/balcony'
-                     *          from='romeo@montague.lit/orchard'
-                     *          type='chat'>
-                     *          <body>Call me but love, and I'll be new baptized; Henceforth I never will be Romeo.</body>
-                     *      </message>
-                     *      </forwarded>
-                     *  </result>
-                     *  </message>
-                     */
-                    var msg1 = $msg({'id':'aeb213', 'to':'juliet@capulet.lit/chamber'})
-                                .c('result',  {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73623'})
-                                    .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
-                                        .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
-                                        .c('message', {
-                                            'xmlns':'jabber:client',
-                                            'to':'juliet@capulet.lit/balcony',
-                                            'from':'romeo@montague.lit/orchard',
-                                            'type':'chat' })
-                                        .c('body').t("Call me but love, and I'll be new baptized;");
-                    _converse.connection._dataRecv(test_utils.createRequest(msg1));
-
-                    var msg2 = $msg({'id':'aeb213', 'to':'juliet@capulet.lit/chamber'})
-                                .c('result',  {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73624'})
-                                    .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
-                                        .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
-                                        .c('message', {
-                                            'xmlns':'jabber:client',
-                                            'to':'juliet@capulet.lit/balcony',
-                                            'from':'romeo@montague.lit/orchard',
-                                            'type':'chat' })
-                                        .c('body').t("Henceforth I never will be Romeo.");
-                    _converse.connection._dataRecv(test_utils.createRequest(msg2));
-
-                    /* Send an <iq> stanza to indicate the end of the result set.
-                     *
-                     * <iq type='result' id='juliet1'>
-                     *     <fin xmlns='urn:xmpp:mam:2'>
-                     *     <set xmlns='http://jabber.org/protocol/rsm'>
-                     *         <first index='0'>28482-98726-73623</first>
-                     *         <last>09af3-cc343-b409f</last>
-                     *         <count>20</count>
-                     *     </set>
-                     * </iq>
-                     */
-                    var stanza = $iq({'type': 'result', 'id': IQ_id})
-                        .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
-                            .c('set',  {'xmlns': 'http://jabber.org/protocol/rsm'})
-                                .c('first', {'index': '0'}).t('23452-4534-1').up()
-                                .c('last').t('09af3-cc343-b409f').up()
-                                .c('count').t('16');
-                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                mock.initConverseWithPromises(
+                    null, [], {},
+                    async function (done, _converse) {
 
-                    expect(callback).toHaveBeenCalled();
-                    var args = callback.calls.argsFor(0);
-                    expect(args[0].length).toBe(2);
-                    expect(args[0][0].outerHTML).toBe(msg1.nodeTree.outerHTML);
-                    expect(args[0][1].outerHTML).toBe(msg2.nodeTree.outerHTML);
-                    expect(args[1]['with']).toBe('romeo@capulet.lit'); // eslint-disable-line dot-notation
-                    expect(args[1].max).toBe('10');
-                    expect(args[1].count).toBe('16');
-                    expect(args[1].first).toBe('23452-4534-1');
-                    expect(args[1].last).toBe('09af3-cc343-b409f');
-                    done()
+                const entity = await _converse.api.disco.entities.get(_converse.domain);
+                if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
+                    _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
+                }
+                let sent_stanza, IQ_id;
+                const sendIQ = _converse.connection.sendIQ;
+                spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
+                    sent_stanza = iq;
+                    IQ_id = sendIQ.bind(this)(iq, callback, errback);
                 });
+                const callback = jasmine.createSpy('callback');
+
+                _converse.api.archive.query({'with': 'romeo@capulet.lit', 'max':'10'}, callback);
+                const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
+
+                /*  <message id='aeb213' to='juliet@capulet.lit/chamber'>
+                 *  <result xmlns='urn:xmpp:mam:2' queryid='f27' id='28482-98726-73623'>
+                 *      <forwarded xmlns='urn:xmpp:forward:0'>
+                 *      <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>
+                 *      <message xmlns='jabber:client'
+                 *          to='juliet@capulet.lit/balcony'
+                 *          from='romeo@montague.lit/orchard'
+                 *          type='chat'>
+                 *          <body>Call me but love, and I'll be new baptized; Henceforth I never will be Romeo.</body>
+                 *      </message>
+                 *      </forwarded>
+                 *  </result>
+                 *  </message>
+                 */
+                var msg1 = $msg({'id':'aeb213', 'to':'juliet@capulet.lit/chamber'})
+                            .c('result',  {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73623'})
+                                .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
+                                    .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
+                                    .c('message', {
+                                        'xmlns':'jabber:client',
+                                        'to':'juliet@capulet.lit/balcony',
+                                        'from':'romeo@montague.lit/orchard',
+                                        'type':'chat' })
+                                    .c('body').t("Call me but love, and I'll be new baptized;");
+                _converse.connection._dataRecv(test_utils.createRequest(msg1));
+
+                var msg2 = $msg({'id':'aeb213', 'to':'juliet@capulet.lit/chamber'})
+                            .c('result',  {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73624'})
+                                .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
+                                    .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
+                                    .c('message', {
+                                        'xmlns':'jabber:client',
+                                        'to':'juliet@capulet.lit/balcony',
+                                        'from':'romeo@montague.lit/orchard',
+                                        'type':'chat' })
+                                    .c('body').t("Henceforth I never will be Romeo.");
+                _converse.connection._dataRecv(test_utils.createRequest(msg2));
+
+                /* Send an <iq> stanza to indicate the end of the result set.
+                 *
+                 * <iq type='result' id='juliet1'>
+                 *     <fin xmlns='urn:xmpp:mam:2'>
+                 *     <set xmlns='http://jabber.org/protocol/rsm'>
+                 *         <first index='0'>28482-98726-73623</first>
+                 *         <last>09af3-cc343-b409f</last>
+                 *         <count>20</count>
+                 *     </set>
+                 * </iq>
+                 */
+                const stanza = $iq({'type': 'result', 'id': IQ_id})
+                    .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
+                        .c('set',  {'xmlns': 'http://jabber.org/protocol/rsm'})
+                            .c('first', {'index': '0'}).t('23452-4534-1').up()
+                            .c('last').t('09af3-cc343-b409f').up()
+                            .c('count').t('16');
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                await test_utils.waitUntil(() => callback.calls.count());
+                expect(callback).toHaveBeenCalled();
+                var args = callback.calls.argsFor(0);
+                expect(args[0].length).toBe(2);
+                expect(args[0][0].outerHTML).toBe(msg1.nodeTree.outerHTML);
+                expect(args[0][1].outerHTML).toBe(msg2.nodeTree.outerHTML);
+                expect(args[1]['with']).toBe('romeo@capulet.lit'); // eslint-disable-line dot-notation
+                expect(args[1].max).toBe('10');
+                expect(args[1].count).toBe('16');
+                expect(args[1].first).toBe('23452-4534-1');
+                expect(args[1].last).toBe('09af3-cc343-b409f');
+                done()
            }));
         });
 
         describe("The default preference", function () {
 
             it("is set once server support for MAM has been confirmed",
-                    mock.initConverseWithPromises(
-                        null, [], {},
-                        function (done, _converse) {
-
-                _converse.api.disco.entities.get(_converse.domain).then(function (entity) {
-                    var sent_stanza, IQ_id;
-                    var sendIQ = _converse.connection.sendIQ;
-                    spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
-                        sent_stanza = iq;
-                        IQ_id = sendIQ.bind(this)(iq, callback, errback);
-                    });
-                    spyOn(_converse, 'onMAMPreferences').and.callThrough();
-                    _converse.message_archiving = 'never';
-
-                    var feature = new Backbone.Model({
-                        'var': Strophe.NS.MAM
-                    });
-                    spyOn(feature, 'save').and.callFake(feature.set); // Save will complain about a url not being set
-
-                    entity.onFeatureAdded(feature);
-
-                    expect(_converse.connection.sendIQ).toHaveBeenCalled();
-                    expect(sent_stanza.toLocaleString()).toBe(
-                        `<iq id="${IQ_id}" type="get" xmlns="jabber:client">`+
-                            `<prefs xmlns="urn:xmpp:mam:2"/>`+
-                        `</iq>`);
-
-                    /* Example 20. Server responds with current preferences
-                     *
-                     * <iq type='result' id='juliet2'>
-                     *   <prefs xmlns='urn:xmpp:mam:0' default='roster'>
-                     *     <always/>
-                     *     <never/>
-                     *   </prefs>
-                     * </iq>
-                     */
-                    var stanza = $iq({'type': 'result', 'id': IQ_id})
-                        .c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'roster'})
-                        .c('always').c('jid').t('romeo@montague.lit').up().up()
-                        .c('never').c('jid').t('montague@montague.lit');
-                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                mock.initConverseWithPromises(
+                    null, [], {},
+                    async function (done, _converse) {
 
-                    expect(_converse.onMAMPreferences).toHaveBeenCalled();
-                    expect(_converse.connection.sendIQ.calls.count()).toBe(2);
+                const entity = await _converse.api.disco.entities.get(_converse.domain);
+                let  sent_stanza, IQ_id;
+                const sendIQ = _converse.connection.sendIQ;
+                spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
+                    sent_stanza = iq;
+                    IQ_id = sendIQ.bind(this)(iq, callback, errback);
+                });
+                spyOn(_converse, 'onMAMPreferences').and.callThrough();
+                _converse.message_archiving = 'never';
 
-                    expect(sent_stanza.toString()).toBe(
-                        `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
-                            `<prefs default="never" xmlns="urn:xmpp:mam:2">`+
-                                `<always><jid>romeo@montague.lit</jid></always>`+
-                                `<never><jid>montague@montague.lit</jid></never>`+
-                            `</prefs>`+
-                        `</iq>`
-                    );
+                var feature = new Backbone.Model({
+                    'var': Strophe.NS.MAM
+                });
+                spyOn(feature, 'save').and.callFake(feature.set); // Save will complain about a url not being set
+
+                entity.onFeatureAdded(feature);
+
+                expect(_converse.connection.sendIQ).toHaveBeenCalled();
+                expect(sent_stanza.toLocaleString()).toBe(
+                    `<iq id="${IQ_id}" type="get" xmlns="jabber:client">`+
+                        `<prefs xmlns="urn:xmpp:mam:2"/>`+
+                    `</iq>`);
+
+                /* Example 20. Server responds with current preferences
+                 *
+                 * <iq type='result' id='juliet2'>
+                 *   <prefs xmlns='urn:xmpp:mam:0' default='roster'>
+                 *     <always/>
+                 *     <never/>
+                 *   </prefs>
+                 * </iq>
+                 */
+                let stanza = $iq({'type': 'result', 'id': IQ_id})
+                    .c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'roster'})
+                    .c('always').c('jid').t('romeo@montague.lit').up().up()
+                    .c('never').c('jid').t('montague@montague.lit');
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                await test_utils.waitUntil(() => _converse.onMAMPreferences.calls.count());
+                expect(_converse.onMAMPreferences).toHaveBeenCalled();
+                expect(_converse.connection.sendIQ.calls.count()).toBe(2);
 
-                    expect(feature.get('preference')).toBe(undefined);
-                    /* <iq type='result' id='juliet3'>
-                     *   <prefs xmlns='urn:xmpp:mam:0' default='always'>
-                     *       <always>
-                     *          <jid>romeo@montague.lit</jid>
-                     *       </always>
-                     *       <never>
-                     *          <jid>montague@montague.lit</jid>
-                     *       </never>
-                     *   </prefs>
-                     * </iq>
-                     */
-                    stanza = $iq({'type': 'result', 'id': IQ_id})
-                        .c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'always'})
+                expect(sent_stanza.toString()).toBe(
+                    `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
+                        `<prefs default="never" xmlns="urn:xmpp:mam:2">`+
+                            `<always><jid>romeo@montague.lit</jid></always>`+
+                            `<never><jid>montague@montague.lit</jid></never>`+
+                        `</prefs>`+
+                    `</iq>`
+                );
+
+                expect(feature.get('preference')).toBe(undefined);
+                /* <iq type='result' id='juliet3'>
+                 *   <prefs xmlns='urn:xmpp:mam:0' default='always'>
+                 *       <always>
+                 *          <jid>romeo@montague.lit</jid>
+                 *       </always>
+                 *       <never>
+                 *          <jid>montague@montague.lit</jid>
+                 *       </never>
+                 *   </prefs>
+                 * </iq>
+                 */
+                stanza = $iq({'type': 'result', 'id': IQ_id})
+                    .c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'always'})
                         .c('always').up()
-                        .c('never').up();
-                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
-                    expect(feature.save).toHaveBeenCalled();
-                    expect(feature.get('preferences')['default']).toBe('never'); // eslint-disable-line dot-notation
-                    done();
-                });
+                        .c('never');
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                await test_utils.waitUntil(() => feature.save.calls.count());
+                expect(feature.save).toHaveBeenCalled();
+                expect(feature.get('preferences')['default']).toBe('never'); // eslint-disable-line dot-notation
+                done();
             }));
         });
     });

+ 347 - 362
spec/protocol.js

@@ -52,327 +52,317 @@
                 mock.initConverseWithPromises(
                     null, ['rosterGroupsFetched'],
                     { roster_groups: false },
-                    function (done, _converse) {
+                    async function (done, _converse) {
 
                 var contact, sent_stanza, IQ_id, stanza, modal;
-                test_utils.waitUntilDiscoConfirmed(_converse, 'localhost', [], ['vcard-temp'])
-                .then(function () {
-                    return test_utils.waitUntil(function () {
-                        return _converse.xmppstatus.vcard.get('fullname');
-                    }, 300);
-                }).then(function () {
-                    /* The process by which a user subscribes to a contact, including
-                     * the interaction between roster items and subscription states.
-                     */
-                    test_utils.openControlBox(_converse);
-                    var cbview = _converse.chatboxviews.get('controlbox');
-
-                    spyOn(_converse.roster, "addAndSubscribe").and.callThrough();
-                    spyOn(_converse.roster, "addContactToRoster").and.callThrough();
-                    spyOn(_converse.roster, "sendContactAddIQ").and.callThrough();
-                    spyOn(_converse.api.vcard, "get").and.callThrough();
-
-                    var sendIQ = _converse.connection.sendIQ;
-                    spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
-                        sent_stanza = iq;
-                        IQ_id = sendIQ.bind(this)(iq, callback, errback);
-                    });
-
-                    cbview.el.querySelector('.add-contact').click()
-                    modal = _converse.rosterview.add_contact_modal;
-                    return test_utils.waitUntil(function () {
-                        return u.isVisible(modal.el);
-                    }, 1000);
-                }).then(function () {
-                    spyOn(modal, "addContactFromForm").and.callThrough();
-                    modal.delegateEvents();
-
-                    // Fill in the form and submit
-                    var form = modal.el.querySelector('form.add-xmpp-contact');
-                    form.querySelector('input').value = 'contact@example.org';
-                    form.querySelector('[type="submit"]').click();
-
-                    /* In preparation for being able to render the contact in the
-                    * user's client interface and for the server to keep track of the
-                    * subscription, the user's client SHOULD perform a "roster set"
-                    * for the new roster item.
-                    */
-                    expect(modal.addContactFromForm).toHaveBeenCalled();
-                    expect(_converse.roster.addAndSubscribe).toHaveBeenCalled();
-                    expect(_converse.roster.addContactToRoster).toHaveBeenCalled();
-
-                    /* _converse request consists of sending an IQ
-                     * stanza of type='set' containing a <query/> element qualified by
-                     * the 'jabber:iq:roster' namespace, which in turn contains an
-                     * <item/> element that defines the new roster item; the <item/>
-                     * element MUST possess a 'jid' attribute, MAY possess a 'name'
-                     * attribute, MUST NOT possess a 'subscription' attribute, and MAY
-                     * contain one or more <group/> child elements:
-                     *
-                     *   <iq type='set' id='set1'>
-                     *   <query xmlns='jabber:iq:roster'>
-                     *       <item
-                     *           jid='contact@example.org'
-                     *           name='MyContact'>
-                     *       <group>MyBuddies</group>
-                     *       </item>
-                     *   </query>
-                     *   </iq>
-                     */
-                    expect(_converse.roster.sendContactAddIQ).toHaveBeenCalled();
-                    expect(sent_stanza.toLocaleString()).toBe(
-                        `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
-                            `<query xmlns="jabber:iq:roster">`+
-                                `<item jid="contact@example.org" name="contact@example.org"/>`+
-                            `</query>`+
-                        `</iq>`
-                    );
-                    /* As a result, the user's server (1) MUST initiate a roster push
-                     * for the new roster item to all available resources associated
-                     * with _converse user that have requested the roster, setting the
-                     * 'subscription' attribute to a value of "none"; and (2) MUST
-                     * reply to the sending resource with an IQ result indicating the
-                     * success of the roster set:
-                     *
-                     * <iq type='set'>
-                     *     <query xmlns='jabber:iq:roster'>
-                     *         <item
-                     *             jid='contact@example.org'
-                     *             subscription='none'
-                     *             name='MyContact'>
-                     *         <group>MyBuddies</group>
-                     *         </item>
-                     *     </query>
-                     * </iq>
-                     */
-                    var create = _converse.roster.create;
-                    var sent_stanzas = [];
-                    spyOn(_converse.connection, 'send').and.callFake(function (stanza) {
-                        sent_stanza = stanza;
-                        sent_stanzas.push(stanza.toLocaleString());
-                    });
-                    spyOn(_converse.roster, 'create').and.callFake(function () {
-                        contact = create.apply(_converse.roster, arguments);
-                        spyOn(contact, 'subscribe').and.callThrough();
-                        return contact;
-                    });
-                    stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'})
-                        .c('item', {
-                            'jid': 'contact@example.org',
-                            'subscription': 'none',
-                            'name': 'contact@example.org'});
-                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
-                    /*
-                    * <iq type='result' id='set1'/>
-                    */
-                    stanza = $iq({'type': 'result', 'id':IQ_id});
-                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
-
-                    // A contact should now have been created
-                    expect(_converse.roster.get('contact@example.org') instanceof _converse.RosterContact).toBeTruthy();
-                    expect(contact.get('jid')).toBe('contact@example.org');
-                    expect(_converse.api.vcard.get).toHaveBeenCalled();
-
-                    /* To subscribe to the contact's presence information,
-                     * the user's client MUST send a presence stanza of
-                     * type='subscribe' to the contact:
-                     *
-                     *  <presence to='contact@example.org' type='subscribe'/>
-                     */
-                    return test_utils.waitUntil(function () {
-                        return sent_stanzas.length == 1;
-                    }, 300);
-                }).then(function () {
-                    expect(contact.subscribe).toHaveBeenCalled();
-                    expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec)
-                        `<presence to="contact@example.org" type="subscribe" xmlns="jabber:client">`+
-                            `<nick xmlns="http://jabber.org/protocol/nick">Max Mustermann</nick>`+
-                        `</presence>`
-                    );
-                    /* As a result, the user's server MUST initiate a second roster
-                     * push to all of the user's available resources that have
-                     * requested the roster, setting the contact to the pending
-                     * sub-state of the 'none' subscription state; _converse pending
-                     * sub-state is denoted by the inclusion of the ask='subscribe'
-                     * attribute in the roster item:
-                     *
-                     *  <iq type='set'>
-                     *    <query xmlns='jabber:iq:roster'>
-                     *      <item
-                     *          jid='contact@example.org'
-                     *          subscription='none'
-                     *          ask='subscribe'
-                     *          name='MyContact'>
-                     *      <group>MyBuddies</group>
-                     *      </item>
-                     *    </query>
-                     *  </iq>
-                     */
-                    spyOn(_converse.roster, "updateContact").and.callThrough();
-                    stanza = $iq({'type': 'set', 'from': _converse.bare_jid})
-                        .c('query', {'xmlns': 'jabber:iq:roster'})
-                        .c('item', {
-                            'jid': 'contact@example.org',
-                            'subscription': 'none',
-                            'ask': 'subscribe',
-                            'name': 'contact@example.org'});
-                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
-                    expect(_converse.roster.updateContact).toHaveBeenCalled();
-                    // Check that the user is now properly shown as a pending
-                    // contact in the roster.
-                    return test_utils.waitUntil(function () {
-                        var $header = $('a:contains("Pending contacts")');
-                        var $contacts = $header.parent().find('li:visible');
-                        return $contacts.length;
-                    }, 600);
-                }).then(function () {
+                await test_utils.waitUntilDiscoConfirmed(_converse, 'localhost', [], ['vcard-temp']);
+                await test_utils.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'), 300);
+                /* The process by which a user subscribes to a contact, including
+                 * the interaction between roster items and subscription states.
+                 */
+                test_utils.openControlBox(_converse);
+                const cbview = _converse.chatboxviews.get('controlbox');
+
+                spyOn(_converse.roster, "addAndSubscribe").and.callThrough();
+                spyOn(_converse.roster, "addContactToRoster").and.callThrough();
+                spyOn(_converse.roster, "sendContactAddIQ").and.callThrough();
+                spyOn(_converse.api.vcard, "get").and.callThrough();
+
+                const sendIQ = _converse.connection.sendIQ;
+                spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
+                    sent_stanza = iq;
+                    IQ_id = sendIQ.bind(this)(iq, callback, errback);
+                });
+
+                cbview.el.querySelector('.add-contact').click()
+                modal = _converse.rosterview.add_contact_modal;
+                await test_utils.waitUntil(() => u.isVisible(modal.el), 1000);
+                spyOn(modal, "addContactFromForm").and.callThrough();
+                modal.delegateEvents();
+
+                // Fill in the form and submit
+                const form = modal.el.querySelector('form.add-xmpp-contact');
+                form.querySelector('input').value = 'contact@example.org';
+                form.querySelector('[type="submit"]').click();
+
+                /* In preparation for being able to render the contact in the
+                * user's client interface and for the server to keep track of the
+                * subscription, the user's client SHOULD perform a "roster set"
+                * for the new roster item.
+                */
+                expect(modal.addContactFromForm).toHaveBeenCalled();
+                expect(_converse.roster.addAndSubscribe).toHaveBeenCalled();
+                expect(_converse.roster.addContactToRoster).toHaveBeenCalled();
+
+                /* _converse request consists of sending an IQ
+                 * stanza of type='set' containing a <query/> element qualified by
+                 * the 'jabber:iq:roster' namespace, which in turn contains an
+                 * <item/> element that defines the new roster item; the <item/>
+                 * element MUST possess a 'jid' attribute, MAY possess a 'name'
+                 * attribute, MUST NOT possess a 'subscription' attribute, and MAY
+                 * contain one or more <group/> child elements:
+                 *
+                 *   <iq type='set' id='set1'>
+                 *   <query xmlns='jabber:iq:roster'>
+                 *       <item
+                 *           jid='contact@example.org'
+                 *           name='MyContact'>
+                 *       <group>MyBuddies</group>
+                 *       </item>
+                 *   </query>
+                 *   </iq>
+                 */
+                expect(_converse.roster.sendContactAddIQ).toHaveBeenCalled();
+                expect(sent_stanza.toLocaleString()).toBe(
+                    `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
+                        `<query xmlns="jabber:iq:roster">`+
+                            `<item jid="contact@example.org" name="contact@example.org"/>`+
+                        `</query>`+
+                    `</iq>`
+                );
+                /* As a result, the user's server (1) MUST initiate a roster push
+                 * for the new roster item to all available resources associated
+                 * with _converse user that have requested the roster, setting the
+                 * 'subscription' attribute to a value of "none"; and (2) MUST
+                 * reply to the sending resource with an IQ result indicating the
+                 * success of the roster set:
+                 *
+                 * <iq type='set'>
+                 *     <query xmlns='jabber:iq:roster'>
+                 *         <item
+                 *             jid='contact@example.org'
+                 *             subscription='none'
+                 *             name='MyContact'>
+                 *         <group>MyBuddies</group>
+                 *         </item>
+                 *     </query>
+                 * </iq>
+                 */
+                const create = _converse.roster.create;
+                const sent_stanzas = [];
+                spyOn(_converse.connection, 'send').and.callFake(function (stanza) {
+                    sent_stanza = stanza;
+                    sent_stanzas.push(stanza.toLocaleString());
+                });
+                spyOn(_converse.roster, 'create').and.callFake(function () {
+                    contact = create.apply(_converse.roster, arguments);
+                    spyOn(contact, 'subscribe').and.callThrough();
+                    return contact;
+                });
+                stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'})
+                    .c('item', {
+                        'jid': 'contact@example.org',
+                        'subscription': 'none',
+                        'name': 'contact@example.org'});
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                /* <iq type='result' id='set1'/>
+                 */
+                stanza = $iq({'type': 'result', 'id':IQ_id});
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                await test_utils.waitUntil(() => _converse.roster.create.calls.count());
+
+                // A contact should now have been created
+                expect(_converse.roster.get('contact@example.org') instanceof _converse.RosterContact).toBeTruthy();
+                expect(contact.get('jid')).toBe('contact@example.org');
+                expect(_converse.api.vcard.get).toHaveBeenCalled();
+
+                /* To subscribe to the contact's presence information,
+                 * the user's client MUST send a presence stanza of
+                 * type='subscribe' to the contact:
+                 *
+                 *  <presence to='contact@example.org' type='subscribe'/>
+                 */
+                await test_utils.waitUntil(() => sent_stanzas.filter(s => s.match('presence')));
+                expect(contact.subscribe).toHaveBeenCalled();
+                expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec)
+                    `<presence to="contact@example.org" type="subscribe" xmlns="jabber:client">`+
+                        `<nick xmlns="http://jabber.org/protocol/nick">Max Mustermann</nick>`+
+                    `</presence>`
+                );
+                /* As a result, the user's server MUST initiate a second roster
+                 * push to all of the user's available resources that have
+                 * requested the roster, setting the contact to the pending
+                 * sub-state of the 'none' subscription state; _converse pending
+                 * sub-state is denoted by the inclusion of the ask='subscribe'
+                 * attribute in the roster item:
+                 *
+                 *  <iq type='set'>
+                 *    <query xmlns='jabber:iq:roster'>
+                 *      <item
+                 *          jid='contact@example.org'
+                 *          subscription='none'
+                 *          ask='subscribe'
+                 *          name='MyContact'>
+                 *      <group>MyBuddies</group>
+                 *      </item>
+                 *    </query>
+                 *  </iq>
+                 */
+                spyOn(_converse.roster, "updateContact").and.callThrough();
+                stanza = $iq({'type': 'set', 'from': _converse.bare_jid})
+                    .c('query', {'xmlns': 'jabber:iq:roster'})
+                    .c('item', {
+                        'jid': 'contact@example.org',
+                        'subscription': 'none',
+                        'ask': 'subscribe',
+                        'name': 'contact@example.org'});
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                expect(_converse.roster.updateContact).toHaveBeenCalled();
+                // Check that the user is now properly shown as a pending
+                // contact in the roster.
+                await test_utils.waitUntil(function () {
                     var $header = $('a:contains("Pending contacts")');
-                    var $contacts = $header.parent().find('li');
-                    expect($contacts.length).toBe(1);
-                    expect($contacts.is(':visible')).toBeTruthy();
-
-                    spyOn(contact, "ackSubscribe").and.callThrough();
-                    /* Here we assume the "happy path" that the contact
-                     * approves the subscription request
-                     *
-                     *  <presence
-                     *      to='user@example.com'
-                     *      from='contact@example.org'
-                     *      type='subscribed'/>
-                     */
-                    stanza = $pres({
-                        'to': _converse.bare_jid,
-                        'from': 'contact@example.org',
-                        'type': 'subscribed'
-                    });
-                    sent_stanza = ""; // Reset
-                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
-                    /* Upon receiving the presence stanza of type "subscribed",
-                     * the user SHOULD acknowledge receipt of that
-                     * subscription state notification by sending a presence
-                     * stanza of type "subscribe".
-                     */
-                    expect(contact.ackSubscribe).toHaveBeenCalled();
-                    expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec)
-                        `<presence to="contact@example.org" type="subscribe" xmlns="jabber:client"/>`
-                    );
-
-                    /* The user's server MUST initiate a roster push to all of the user's
-                     * available resources that have requested the roster,
-                     * containing an updated roster item for the contact with
-                     * the 'subscription' attribute set to a value of "to";
-                     *
-                     *  <iq type='set'>
-                     *    <query xmlns='jabber:iq:roster'>
-                     *      <item
-                     *          jid='contact@example.org'
-                     *          subscription='to'
-                     *          name='MyContact'>
-                     *        <group>MyBuddies</group>
-                     *      </item>
-                     *    </query>
-                     *  </iq>
-                     */
-                    IQ_id = _converse.connection.getUniqueId('roster');
-                    stanza = $iq({'type': 'set', 'id': IQ_id})
-                        .c('query', {'xmlns': 'jabber:iq:roster'})
-                        .c('item', {
-                            'jid': 'contact@example.org',
-                            'subscription': 'to',
-                            'name': 'contact@example.org'});
-                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
-                    // Check that the IQ set was acknowledged.
-                    expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec)
-                        `<iq from="dummy@localhost/resource" id="${IQ_id}" type="result" xmlns="jabber:client"/>`
-                    );
-                    expect(_converse.roster.updateContact).toHaveBeenCalled();
-
-                    // The contact should now be visible as an existing
-                    // contact (but still offline).
-                    return test_utils.waitUntil(function () {
-                        var $header = $('a:contains("My contacts")');
-                        var $contacts = $header.parent().find('li:visible');
-                        return $contacts.length;
-                    }, 600);
-                }).then(function () {
-                    var $header = $('a:contains("My contacts")');
-                    expect($header.length).toBe(1);
-                    expect($header.is(":visible")).toBeTruthy();
-                    var $contacts = $header.parent().find('li');
-                    expect($contacts.length).toBe(1);
-                    // Check that it has the right classes and text
-                    expect($contacts.hasClass('to')).toBeTruthy();
-                    expect($contacts.hasClass('both')).toBeFalsy();
-                    expect($contacts.hasClass('current-xmpp-contact')).toBeTruthy();
-                    expect($contacts.text().trim()).toBe('Contact');
-                    expect(contact.presence.get('show')).toBe('offline');
-
-                    /*  <presence
-                     *      from='contact@example.org/resource'
-                     *      to='user@example.com/resource'/>
-                     */
-                    stanza = $pres({'to': _converse.bare_jid, 'from': 'contact@example.org/resource'});
-                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
-                    // Now the contact should also be online.
-                    expect(contact.presence.get('show')).toBe('online');
-
-                    /* Section 8.3.  Creating a Mutual Subscription
-                     *
-                     * If the contact wants to create a mutual subscription,
-                     * the contact MUST send a subscription request to the
-                     * user.
-                     *
-                     * <presence from='contact@example.org' to='user@example.com' type='subscribe'/>
-                     */
-                    spyOn(contact, 'authorize').and.callThrough();
-                    spyOn(_converse.roster, 'handleIncomingSubscription').and.callThrough();
-                    stanza = $pres({
-                        'to': _converse.bare_jid,
-                        'from': 'contact@example.org/resource',
-                        'type': 'subscribe'});
-                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
-                    expect(_converse.roster.handleIncomingSubscription).toHaveBeenCalled();
-
-                    /* The user's client MUST send a presence stanza of type
-                     * "subscribed" to the contact in order to approve the
-                     * subscription request.
-                     *
-                     *  <presence to='contact@example.org' type='subscribed'/>
-                     */
-                    expect(contact.authorize).toHaveBeenCalled();
-                    expect(sent_stanza.toLocaleString()).toBe(
-                        `<presence to="contact@example.org" type="subscribed" xmlns="jabber:client"/>`
-                    );
-
-                    /* As a result, the user's server MUST initiate a
-                     * roster push containing a roster item for the
-                     * contact with the 'subscription' attribute set to
-                     * a value of "both".
-                     *
-                     *  <iq type='set'>
-                     *    <query xmlns='jabber:iq:roster'>
-                     *      <item
-                     *          jid='contact@example.org'
-                     *          subscription='both'
-                     *          name='MyContact'>
-                     *      <group>MyBuddies</group>
-                     *      </item>
-                     *    </query>
-                     *  </iq>
-                     */
-                    stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'})
-                        .c('item', {
-                            'jid': 'contact@example.org',
-                            'subscription': 'both',
-                            'name': 'contact@example.org'});
-                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
-                    expect(_converse.roster.updateContact).toHaveBeenCalled();
-
-                    // The class on the contact will now have switched.
-                    expect($contacts.hasClass('to')).toBeFalsy();
-                    expect($contacts.hasClass('both')).toBeTruthy();
-                    done();
+                    var $contacts = $header.parent().find('li:visible');
+                    return $contacts.length;
+                }, 600);
+
+                var $header = $('a:contains("Pending contacts")');
+                var $contacts = $header.parent().find('li');
+                expect($contacts.length).toBe(1);
+                expect($contacts.is(':visible')).toBeTruthy();
+
+                spyOn(contact, "ackSubscribe").and.callThrough();
+                /* Here we assume the "happy path" that the contact
+                 * approves the subscription request
+                 *
+                 *  <presence
+                 *      to='user@example.com'
+                 *      from='contact@example.org'
+                 *      type='subscribed'/>
+                 */
+                stanza = $pres({
+                    'to': _converse.bare_jid,
+                    'from': 'contact@example.org',
+                    'type': 'subscribed'
                 });
+                sent_stanza = ""; // Reset
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                /* Upon receiving the presence stanza of type "subscribed",
+                 * the user SHOULD acknowledge receipt of that
+                 * subscription state notification by sending a presence
+                 * stanza of type "subscribe".
+                 */
+                expect(contact.ackSubscribe).toHaveBeenCalled();
+                expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec)
+                    `<presence to="contact@example.org" type="subscribe" xmlns="jabber:client"/>`
+                );
+
+                /* The user's server MUST initiate a roster push to all of the user's
+                 * available resources that have requested the roster,
+                 * containing an updated roster item for the contact with
+                 * the 'subscription' attribute set to a value of "to";
+                 *
+                 *  <iq type='set'>
+                 *    <query xmlns='jabber:iq:roster'>
+                 *      <item
+                 *          jid='contact@example.org'
+                 *          subscription='to'
+                 *          name='MyContact'>
+                 *        <group>MyBuddies</group>
+                 *      </item>
+                 *    </query>
+                 *  </iq>
+                 */
+                IQ_id = _converse.connection.getUniqueId('roster');
+                stanza = $iq({'type': 'set', 'id': IQ_id})
+                    .c('query', {'xmlns': 'jabber:iq:roster'})
+                    .c('item', {
+                        'jid': 'contact@example.org',
+                        'subscription': 'to',
+                        'name': 'contact@example.org'});
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                // Check that the IQ set was acknowledged.
+                expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec)
+                    `<iq from="dummy@localhost/resource" id="${IQ_id}" type="result" xmlns="jabber:client"/>`
+                );
+                expect(_converse.roster.updateContact).toHaveBeenCalled();
+
+                // The contact should now be visible as an existing
+                // contact (but still offline).
+                await test_utils.waitUntil(function () {
+                    var $header = $('a:contains("My contacts")');
+                    var $contacts = $header.parent().find('li:visible');
+                    return $contacts.length;
+                }, 600);
+                $header = $('a:contains("My contacts")');
+                expect($header.length).toBe(1);
+                expect($header.is(":visible")).toBeTruthy();
+                $contacts = $header.parent().find('li');
+                expect($contacts.length).toBe(1);
+                // Check that it has the right classes and text
+                expect($contacts.hasClass('to')).toBeTruthy();
+                expect($contacts.hasClass('both')).toBeFalsy();
+                expect($contacts.hasClass('current-xmpp-contact')).toBeTruthy();
+                expect($contacts.text().trim()).toBe('Contact');
+                expect(contact.presence.get('show')).toBe('offline');
+
+                /*  <presence
+                 *      from='contact@example.org/resource'
+                 *      to='user@example.com/resource'/>
+                 */
+                stanza = $pres({'to': _converse.bare_jid, 'from': 'contact@example.org/resource'});
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                // Now the contact should also be online.
+                expect(contact.presence.get('show')).toBe('online');
+
+                /* Section 8.3.  Creating a Mutual Subscription
+                 *
+                 * If the contact wants to create a mutual subscription,
+                 * the contact MUST send a subscription request to the
+                 * user.
+                 *
+                 * <presence from='contact@example.org' to='user@example.com' type='subscribe'/>
+                 */
+                spyOn(contact, 'authorize').and.callThrough();
+                spyOn(_converse.roster, 'handleIncomingSubscription').and.callThrough();
+                stanza = $pres({
+                    'to': _converse.bare_jid,
+                    'from': 'contact@example.org/resource',
+                    'type': 'subscribe'});
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                expect(_converse.roster.handleIncomingSubscription).toHaveBeenCalled();
+
+                /* The user's client MUST send a presence stanza of type
+                 * "subscribed" to the contact in order to approve the
+                 * subscription request.
+                 *
+                 *  <presence to='contact@example.org' type='subscribed'/>
+                 */
+                expect(contact.authorize).toHaveBeenCalled();
+                expect(sent_stanza.toLocaleString()).toBe(
+                    `<presence to="contact@example.org" type="subscribed" xmlns="jabber:client"/>`
+                );
+
+                /* As a result, the user's server MUST initiate a
+                 * roster push containing a roster item for the
+                 * contact with the 'subscription' attribute set to
+                 * a value of "both".
+                 *
+                 *  <iq type='set'>
+                 *    <query xmlns='jabber:iq:roster'>
+                 *      <item
+                 *          jid='contact@example.org'
+                 *          subscription='both'
+                 *          name='MyContact'>
+                 *      <group>MyBuddies</group>
+                 *      </item>
+                 *    </query>
+                 *  </iq>
+                 */
+                stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'})
+                    .c('item', {
+                        'jid': 'contact@example.org',
+                        'subscription': 'both',
+                        'name': 'contact@example.org'});
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                expect(_converse.roster.updateContact).toHaveBeenCalled();
+
+                // The class on the contact will now have switched.
+                expect($contacts.hasClass('to')).toBeFalsy();
+                expect($contacts.hasClass('both')).toBeTruthy();
+                done();
+                
             }));
 
             it("Alternate Flow: Contact Declines Subscription Request",
@@ -466,7 +456,7 @@
                 mock.initConverseWithPromises(
                     null, ['rosterGroupsFetched'],
                     { roster_groups: false },
-                    function (done, _converse) {
+                    async function (done, _converse) {
 
                 var sent_IQ, IQ_id, jid = 'annegreet.gomez@localhost';
                 test_utils.openControlBox(_converse);
@@ -480,51 +470,46 @@
                     sent_IQ = iq;
                     IQ_id = sendIQ.bind(this)(iq, callback, errback);
                 });
-                return test_utils.waitUntil(function () {
-                    var $header = $('a:contains("My contacts")');
-                    var $contacts = $header.parent().find('li');
-                    return $contacts.length;
-                }, 600).then(function () {
+                const $header = $('a:contains("My contacts")');
+                await test_utils.waitUntil(() => $header.parent().find('li').length);
+                        
+                // remove the first user
+                $header.parent().find('li .remove-xmpp-contact').get(0).click();
+                expect(window.confirm).toHaveBeenCalled();
+
+                /* Section 8.6 Removing a Roster Item and Cancelling All
+                 * Subscriptions
+                 *
+                 * First the user is removed from the roster
+                 * Because there may be many steps involved in completely
+                 * removing a roster item and cancelling subscriptions in
+                 * both directions, the roster management protocol includes
+                 * a "shortcut" method for doing so. The process may be
+                 * initiated no matter what the current subscription state
+                 * is by sending a roster set containing an item for the
+                 * contact with the 'subscription' attribute set to a value
+                 * of "remove":
+                 *
+                 * <iq type='set' id='remove1'>
+                 *   <query xmlns='jabber:iq:roster'>
+                 *       <item jid='contact@example.org' subscription='remove'/>
+                 *   </query>
+                 * </iq>
+                 */
+                expect(sent_IQ.toLocaleString()).toBe(
+                    `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
+                        `<query xmlns="jabber:iq:roster">`+
+                            `<item jid="annegreet.gomez@localhost" subscription="remove"/>`+
+                        `</query>`+
+                    `</iq>`);
 
-                    var $header = $('a:contains("My contacts")');
-                    // remove the first user
-                    $header.parent().find('li .remove-xmpp-contact').get(0).click();
-                    expect(window.confirm).toHaveBeenCalled();
-
-                    /* Section 8.6 Removing a Roster Item and Cancelling All
-                     * Subscriptions
-                     *
-                     * First the user is removed from the roster
-                     * Because there may be many steps involved in completely
-                     * removing a roster item and cancelling subscriptions in
-                     * both directions, the roster management protocol includes
-                     * a "shortcut" method for doing so. The process may be
-                     * initiated no matter what the current subscription state
-                     * is by sending a roster set containing an item for the
-                     * contact with the 'subscription' attribute set to a value
-                     * of "remove":
-                     *
-                     * <iq type='set' id='remove1'>
-                     *   <query xmlns='jabber:iq:roster'>
-                     *       <item jid='contact@example.org' subscription='remove'/>
-                     *   </query>
-                     * </iq>
-                     */
-                    expect(sent_IQ.toLocaleString()).toBe(
-                        `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
-                            `<query xmlns="jabber:iq:roster">`+
-                                `<item jid="annegreet.gomez@localhost" subscription="remove"/>`+
-                            `</query>`+
-                        `</iq>`);
-
-                    // Receive confirmation from the contact's server
-                    // <iq type='result' id='remove1'/>
-                    var stanza = $iq({'type': 'result', 'id':IQ_id});
-                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
-                    // Our contact has now been removed
-                    expect(typeof _converse.roster.get(jid) === "undefined").toBeTruthy();
-                    done();
-                });
+                // Receive confirmation from the contact's server
+                // <iq type='result' id='remove1'/>
+                const stanza = $iq({'type': 'result', 'id':IQ_id});
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                // Our contact has now been removed
+                await test_utils.waitUntil(() => typeof _converse.roster.get(jid) === "undefined");
+                done();
             }));
 
             it("Receiving a subscription request", mock.initConverseWithPromises(

+ 114 - 119
spec/roster.js

@@ -1,14 +1,16 @@
 (function (root, factory) {
     define(["jquery", "jasmine", "mock", "test-utils"], factory);
 } (this, function ($, jasmine, mock, test_utils) {
-    var _ = converse.env._;
-    var Strophe = converse.env.Strophe;
-    var $pres = converse.env.$pres;
-    var $msg = converse.env.$msg;
-    var $iq = converse.env.$iq;
-    var u = converse.env.utils;
-
-    var checkHeaderToggling = function (group) {
+    const $iq = converse.env.$iq;
+    const $msg = converse.env.$msg;
+    const $pres = converse.env.$pres;
+    const Strophe = converse.env.Strophe;
+    const _ = converse.env._;
+    const sizzle = converse.env.sizzle;
+    const u = converse.env.utils;
+
+
+    const checkHeaderToggling = function (group) {
         var $group = $(group);
         var toggle = group.querySelector('a.group-toggle');
         expect(u.isVisible($group[0])).toBeTruthy();
@@ -38,57 +40,58 @@
         it("supports roster versioning",
             mock.initConverseWithPromises(
                 null, ['rosterGroupsFetched'], {},
-                function (done, _converse) {
+                async function (done, _converse) {
 
             const IQ_stanzas = _converse.connection.IQ_stanzas;
-            test_utils.waitUntil(
+            let node = await test_utils.waitUntil(
                 () => _.filter(IQ_stanzas, iq => iq.nodeTree.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop()
-            ).then(node => {
-                let stanza = node.nodeTree;
-                expect(_converse.roster.data.get('version')).toBeUndefined();
-                expect(node.toLocaleString()).toBe(
-                    `<iq id="${stanza.getAttribute('id')}" type="get" xmlns="jabber:client">`+
-                        `<query xmlns="jabber:iq:roster"/>`+
-                    `</iq>`);
-                let result = $iq({
-                    'to': _converse.connection.jid,
-                    'type': 'result',
-                    'id': stanza.getAttribute('id')
-                }).c('query', {
-                    'xmlns': 'jabber:iq:roster',
-                    'ver': 'ver7'
-                }).c('item', {'jid': 'nurse@example.com'}).up()
-                  .c('item', {'jid': 'romeo@example.com'})
-                _converse.connection._dataRecv(test_utils.createRequest(result));
-                expect(_converse.roster.data.get('version')).toBe('ver7');
-                expect(_converse.roster.models.length).toBe(2);
-
-                _converse.roster.fetchFromServer();
-                node = _converse.connection.IQ_stanzas.pop();
-                stanza = node.nodeTree;
-                expect(node.toLocaleString()).toBe(
-                    `<iq id="${stanza.getAttribute('id')}" type="get" xmlns="jabber:client">`+
-                        `<query ver="ver7" xmlns="jabber:iq:roster"/>`+
-                    `</iq>`);
-
-                result = $iq({
-                    'to': _converse.connection.jid,
-                    'type': 'result',
-                    'id': stanza.getAttribute('id')
-                });
-                _converse.connection._dataRecv(test_utils.createRequest(result));
-
-                const roster_push = $iq({
-                    'to': _converse.connection.jid,
-                    'type': 'set',
-                }).c('query', {'xmlns': 'jabber:iq:roster', 'ver': 'ver34'})
-                    .c('item', {'jid': 'romeo@example.com', 'subscription': 'remove'});
-                _converse.connection._dataRecv(test_utils.createRequest(roster_push));
-                expect(_converse.roster.data.get('version')).toBe('ver34');
-                expect(_converse.roster.models.length).toBe(1);
-                expect(_converse.roster.at(0).get('jid')).toBe('nurse@example.com');
-                done();
+            );
+            let stanza = node.nodeTree;
+            expect(_converse.roster.data.get('version')).toBeUndefined();
+            expect(node.toLocaleString()).toBe(
+                `<iq id="${stanza.getAttribute('id')}" type="get" xmlns="jabber:client">`+
+                    `<query xmlns="jabber:iq:roster"/>`+
+                `</iq>`);
+            let result = $iq({
+                'to': _converse.connection.jid,
+                'type': 'result',
+                'id': stanza.getAttribute('id')
+            }).c('query', {
+                'xmlns': 'jabber:iq:roster',
+                'ver': 'ver7'
+            }).c('item', {'jid': 'nurse@example.com'}).up()
+              .c('item', {'jid': 'romeo@example.com'})
+            _converse.connection._dataRecv(test_utils.createRequest(result));
+
+            await test_utils.waitUntil(() => _converse.roster.models.length > 1);
+            expect(_converse.roster.data.get('version')).toBe('ver7');
+            expect(_converse.roster.models.length).toBe(2);
+
+            _converse.roster.fetchFromServer();
+            node = _converse.connection.IQ_stanzas.pop();
+            stanza = node.nodeTree;
+            expect(node.toLocaleString()).toBe(
+                `<iq id="${stanza.getAttribute('id')}" type="get" xmlns="jabber:client">`+
+                    `<query ver="ver7" xmlns="jabber:iq:roster"/>`+
+                `</iq>`);
+
+            result = $iq({
+                'to': _converse.connection.jid,
+                'type': 'result',
+                'id': stanza.getAttribute('id')
             });
+            _converse.connection._dataRecv(test_utils.createRequest(result));
+
+            const roster_push = $iq({
+                'to': _converse.connection.jid,
+                'type': 'set',
+            }).c('query', {'xmlns': 'jabber:iq:roster', 'ver': 'ver34'})
+                .c('item', {'jid': 'romeo@example.com', 'subscription': 'remove'});
+            _converse.connection._dataRecv(test_utils.createRequest(roster_push));
+            expect(_converse.roster.data.get('version')).toBe('ver34');
+            expect(_converse.roster.models.length).toBe(1);
+            expect(_converse.roster.at(0).get('jid')).toBe('nurse@example.com');
+            done();
         }));
 
         describe("The live filter", function () {
@@ -549,17 +552,15 @@
             it("can be collapsed under their own header", 
                 mock.initConverseWithPromises(
                     null, ['rosterGroupsFetched'], {},
-                    function (done, _converse) {
+                    async function (done, _converse) {
 
                 _addContacts(_converse);
-                test_utils.waitUntil(function () {
-                    return $(_converse.rosterview.el).find('.roster-group:visible li').length;
-                }, 500).then(function () {
-                    checkHeaderToggling.apply(
-                        _converse,
-                        [_converse.rosterview.get('Pending contacts').el]
-                    ).then(done);
-                });
+                await test_utils.waitUntil(() => $(_converse.rosterview.el).find('.roster-group:visible li').length, 1000);
+                await checkHeaderToggling.apply(
+                    _converse,
+                    [_converse.rosterview.get('Pending contacts').el]
+                );
+                done();
             }));
 
             it("can be added to the roster",
@@ -662,10 +663,10 @@
             it("do not have a header if there aren't any", 
                 mock.initConverseWithPromises(
                     null, ['rosterGroupsFetched'], {},
-                    function (done, _converse) {
+                    async function (done, _converse) {
 
                 test_utils.openControlBox();
-                var name = mock.pend_names[0];
+                const name = mock.pend_names[0];
                 _converse.roster.create({
                     jid: name.replace(/ /g,'.').toLowerCase() + '@localhost',
                     subscription: 'none',
@@ -676,17 +677,18 @@
                 spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback) {
                     if (typeof callback === "function") { return callback(); }
                 });
-                test_utils.waitUntil(function () {
+                await test_utils.waitUntil(function () {
                     var $pending_contacts = $(_converse.rosterview.get('Pending contacts').el);
                     return $pending_contacts.is(':visible') && $pending_contacts.find('li:visible').length;
-                }, 700).then(function () {
-                    $(_converse.rosterview.el).find(".pending-contact-name:contains('"+name+"')")
-                        .parent().siblings('.remove-xmpp-contact')[0].click();
-                    expect(window.confirm).toHaveBeenCalled();
-                    expect(_converse.connection.sendIQ).toHaveBeenCalled();
-                    expect(u.isVisible(_converse.rosterview.get('Pending contacts').el)).toEqual(false);
-                    done();
-                });
+                }, 700)
+                            
+                $(_converse.rosterview.el).find(".pending-contact-name:contains('"+name+"')")
+                    .parent().siblings('.remove-xmpp-contact')[0].click();
+                expect(window.confirm).toHaveBeenCalled();
+                expect(_converse.connection.sendIQ).toHaveBeenCalled();
+
+                await test_utils.waitUntil(() => !u.isVisible(_converse.rosterview.get('Pending contacts').el));
+                done();
             }));
 
             it("is shown when a new private message is received",
@@ -822,42 +824,39 @@
             it("can be removed by the user", 
                 mock.initConverseWithPromises(
                     null, ['rosterGroupsFetched'], {},
-                    function (done, _converse) {
+                    async function (done, _converse) {
 
-                var sent_IQ;
                 _addContacts(_converse);
-                test_utils.waitUntil(function () {
-                    return $(_converse.rosterview.el).find('li').length;
-                }, 500).then(function () {
-                    var name = mock.cur_names[0];
-                    var jid = name.replace(/ /g,'.').toLowerCase() + '@localhost';
-                    var contact = _converse.roster.get(jid);
-                    spyOn(window, 'confirm').and.returnValue(true);
-                    spyOn(contact, 'removeFromRoster').and.callThrough();
-
-                    var sendIQ = _converse.connection.sendIQ;
-                    spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
-                        sent_IQ = iq;
-                        callback();
-                    });
-                    $(_converse.rosterview.el).find(".open-chat:contains('"+name+"')")
-                        .parent().find('.remove-xmpp-contact')[0].click();
+                await test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('li').length);
+                const name = mock.cur_names[0];
+                const jid = name.replace(/ /g,'.').toLowerCase() + '@localhost';
+                const contact = _converse.roster.get(jid);
+                spyOn(window, 'confirm').and.returnValue(true);
+                spyOn(contact, 'removeFromRoster').and.callThrough();
 
-                    expect(window.confirm).toHaveBeenCalled();
-                    expect(sent_IQ.toLocaleString()).toBe(
-                        `<iq type="set" xmlns="jabber:client">`+
-                            `<query xmlns="jabber:iq:roster"><item jid="max.frankfurter@localhost" subscription="remove"/></query>`+
-                        `</iq>`);
-                    expect(contact.removeFromRoster).toHaveBeenCalled();
-                    expect($(_converse.rosterview.el).find(".open-chat:contains('"+name+"')").length).toEqual(0);
-                    done();
+                const sendIQ = _converse.connection.sendIQ;
+                let sent_IQ;
+                spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
+                    sent_IQ = iq;
+                    callback();
                 });
+                $(_converse.rosterview.el).find(".open-chat:contains('"+name+"')")
+                    .parent().find('.remove-xmpp-contact')[0].click();
+
+                expect(window.confirm).toHaveBeenCalled();
+                expect(sent_IQ.toLocaleString()).toBe(
+                    `<iq type="set" xmlns="jabber:client">`+
+                        `<query xmlns="jabber:iq:roster"><item jid="max.frankfurter@localhost" subscription="remove"/></query>`+
+                    `</iq>`);
+                expect(contact.removeFromRoster).toHaveBeenCalled();
+                await test_utils.waitUntil(() => $(_converse.rosterview.el).find(".open-chat:contains('"+name+"')").length === 0);
+                done();
             }));
 
             it("do not have a header if there aren't any", 
                 mock.initConverseWithPromises(
                     null, ['rosterGroupsFetched'], {},
-                    function (done, _converse) {
+                    async function (done, _converse) {
 
                 test_utils.openControlBox();
                 var name = mock.cur_names[0];
@@ -868,24 +867,20 @@
                     ask: null,
                     fullname: name
                 });
-                test_utils.waitUntil(function () {
-                    return $(_converse.rosterview.el).find('.roster-group:visible li').length;
-                }, 700).then(function () {
-                    spyOn(window, 'confirm').and.returnValue(true);
-                    spyOn(contact, 'removeFromRoster').and.callThrough();
-                    spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback) {
-                        if (typeof callback === "function") { return callback(); }
-                    });
-
-                    expect($(_converse.rosterview.el).find('.roster-group').css('display')).toEqual('block');
-                    $(_converse.rosterview.el).find(".open-chat:contains('"+name+"')")
-                        .parent().find('.remove-xmpp-contact')[0].click();
-                    expect(window.confirm).toHaveBeenCalled();
-                    expect(_converse.connection.sendIQ).toHaveBeenCalled();
-                    expect(contact.removeFromRoster).toHaveBeenCalled();
-                    expect($(_converse.rosterview.el).find('.roster-group').length).toEqual(0);
-                    done();
+                await test_utils.waitUntil(() => $(_converse.rosterview.el).find('.roster-group:visible li').length, 1000);
+                spyOn(window, 'confirm').and.returnValue(true);
+                spyOn(contact, 'removeFromRoster').and.callThrough();
+                spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback) {
+                    if (typeof callback === "function") { return callback(); }
                 });
+                expect($(_converse.rosterview.el).find('.roster-group').css('display')).toEqual('block');
+                $(_converse.rosterview.el).find(".open-chat:contains('"+name+"')")
+                    .parent().find('.remove-xmpp-contact')[0].click();
+                expect(window.confirm).toHaveBeenCalled();
+                expect(_converse.connection.sendIQ).toHaveBeenCalled();
+                expect(contact.removeFromRoster).toHaveBeenCalled();
+                await test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length === 0);
+                done();
             }));
 
             it("can change their status to online and be sorted alphabetically", 

+ 9 - 11
src/converse-muc-views.js

@@ -367,16 +367,14 @@ converse.plugins.add('converse-muc-views', {
             updateRoomsList () {
                 /* Send an IQ stanza to the server asking for all groupchats
                  */
-                _converse.connection.sendIQ(
-                    $iq({
-                        'to': this.model.get('muc_domain'),
-                        'from': _converse.connection.jid,
-                        'type': "get"
-                    }).c("query", {xmlns: Strophe.NS.DISCO_ITEMS}),
-                    this.onRoomsFound.bind(this),
-                    this.informNoRoomsFound.bind(this),
-                    5000
-                );
+                const iq = $iq({
+                    'to': this.model.get('muc_domain'),
+                    'from': _converse.connection.jid,
+                    'type': "get"
+                }).c("query", {xmlns: Strophe.NS.DISCO_ITEMS});
+                _converse.api.sendIQ(iq)
+                    .then(iq => this.onRoomsFound(iq))
+                    .catch(iq => this.informNoRoomsFound())
             },
 
             showRooms (ev) {
@@ -805,7 +803,7 @@ converse.plugins.add('converse-muc-views', {
                 const item = $build("item", {nick, role});
                 const iq = $iq({to: groupchat, type: "set"}).c("query", {xmlns: Strophe.NS.MUC_ADMIN}).cnode(item.node);
                 if (reason !== null) { iq.c("reason", reason); }
-                return _converse.connection.sendIQ(iq, onSuccess, onError);
+                return _converse.api.sendIQ(iq).then(onSuccess).catch(onError);
             },
 
             verifyRoles (roles) {

+ 2 - 2
src/headless/converse-core.js

@@ -1651,9 +1651,9 @@ _converse.api = {
      * @returns {Promise} A promise which resolves when we receive a `result` stanza
      * or is rejected when we receive an `error` stanza.
      */
-    'sendIQ' (stanza) {
+    'sendIQ' (stanza, timeout) {
         return new Promise((resolve, reject) => {
-            _converse.connection.sendIQ(stanza, resolve, reject, _converse.IQ_TIMEOUT);
+            _converse.connection.sendIQ(stanza, resolve, reject, timeout || _converse.IQ_TIMEOUT);
             _converse.emit('send', stanza); 
         });
     }

+ 20 - 26
src/headless/converse-mam.js

@@ -99,9 +99,8 @@ function queryForArchivedMessages (_converse, options, callback, errback) {
         return true;
     }, Strophe.NS.MAM);
 
-    _converse.connection.sendIQ(
-        stanza,
-        function (iq) {
+    _converse.api.sendIQ(stanza, _converse.message_archiving_timeout)
+        .then(iq => {
             _converse.connection.deleteHandler(message_handler);
             if (_.isFunction(callback)) {
                 const set = iq.querySelector('set');
@@ -112,13 +111,13 @@ function queryForArchivedMessages (_converse, options, callback, errback) {
                 }
                 callback(messages, rsm);
             }
-        },
-        function () {
+        }).catch(e => {
             _converse.connection.deleteHandler(message_handler);
-            if (_.isFunction(errback)) { errback.apply(this, arguments); }
-        },
-        _converse.message_archiving_timeout
-    );
+            if (_.isFunction(errback)) {
+                errback.apply(this, arguments);
+            }
+            return;
+        });
 }
 
 
@@ -333,7 +332,7 @@ converse.plugins.add('converse-mam', {
             message_archiving_timeout: 8000, // Time (in milliseconds) to wait before aborting MAM request
         });
 
-        _converse.onMAMError = function (model, iq) {
+        _converse.onMAMError = function (iq) {
             if (iq.querySelectorAll('feature-not-implemented').length) {
                 _converse.log(
                     "Message Archive Management (XEP-0313) not supported by this server",
@@ -365,17 +364,14 @@ converse.plugins.add('converse-mam', {
                         'xmlns':Strophe.NS.MAM,
                         'default':_converse.message_archiving
                     });
-                _.each(preference.children, function (child) {
-                    stanza.cnode(child).up();
-                });
-                _converse.connection.sendIQ(stanza, _.partial(function (feature, iq) {
-                        // XXX: Strictly speaking, the server should respond with the updated prefs
-                        // (see example 18: https://xmpp.org/extensions/xep-0313.html#config)
-                        // but Prosody doesn't do this, so we don't rely on it.
-                        feature.save({'preferences': {'default':_converse.message_archiving}});
-                    }, feature),
-                    _converse.onMAMError
-                );
+                _.each(preference.children, child => stanza.cnode(child).up());
+
+                // XXX: Strictly speaking, the server should respond with the updated prefs
+                // (see example 18: https://xmpp.org/extensions/xep-0313.html#config)
+                // but Prosody doesn't do this, so we don't rely on it.
+                _converse.api.sendIQ(stanza)
+                    .then(() => feature.save({'preferences': {'default':_converse.message_archiving}}))
+                    .catch(_converse.onMAMError);
             } else {
                 feature.save({'preferences': {'default':_converse.message_archiving}});
             }
@@ -388,11 +384,9 @@ converse.plugins.add('converse-mam', {
                     prefs['default'] !== _converse.message_archiving && // eslint-disable-line dot-notation
                     !_.isUndefined(_converse.message_archiving) ) {
                 // Ask the server for archiving preferences
-                _converse.connection.sendIQ(
-                    $iq({'type': 'get'}).c('prefs', {'xmlns': Strophe.NS.MAM}),
-                    _.partial(_converse.onMAMPreferences, feature),
-                    _.partial(_converse.onMAMError, feature)
-                );
+                _converse.api.sendIQ($iq({'type': 'get'}).c('prefs', {'xmlns': Strophe.NS.MAM}))
+                    .then(_.partial(_converse.onMAMPreferences, feature))
+                    .catch(_converse.onMAMError);
             }
         });
 

+ 16 - 24
src/headless/converse-muc.js

@@ -620,16 +620,10 @@ converse.plugins.add('converse-muc', {
                  * Returns a promise which resolves once the response IQ
                  * has been received.
                  */
-                return new Promise((resolve, reject) => {
-                    _converse.connection.sendIQ(
-                        $iq({
-                            'to': this.get('jid'),
-                            'type': "get"
-                        }).c("query", {xmlns: Strophe.NS.MUC_OWNER}),
-                        resolve,
-                        reject
-                    );
-                });
+                return _converse.api.sendIQ(
+                    $iq({'to': this.get('jid'), 'type': "get"})
+                     .c("query", {xmlns: Strophe.NS.MUC_OWNER})
+                );
             },
 
             sendConfiguration (config, callback, errback) {
@@ -652,7 +646,7 @@ converse.plugins.add('converse-muc', {
                 _.each(config || [], function (node) { iq.cnode(node).up(); });
                 callback = _.isUndefined(callback) ? _.noop : _.partial(callback, iq.nodeTree);
                 errback = _.isUndefined(errback) ? _.noop : _.partial(errback, iq.nodeTree);
-                return _converse.connection.sendIQ(iq, callback, errback);
+                return _converse.api.sendIQ(iq).then(callback).catch(errback);
             },
 
             saveAffiliationAndRole (pres) {
@@ -685,19 +679,17 @@ converse.plugins.add('converse-muc', {
                  *  (Object) member: Map containing the member's jid and
                  *      optionally a reason and affiliation.
                  */
-                return new Promise((resolve, reject) => {
-                    const iq = $iq({to: this.get('jid'), type: "set"})
-                        .c("query", {xmlns: Strophe.NS.MUC_ADMIN})
-                        .c("item", {
-                            'affiliation': member.affiliation || affiliation,
-                            'nick': member.nick,
-                            'jid': member.jid
-                        });
-                    if (!_.isUndefined(member.reason)) {
-                        iq.c("reason", member.reason);
-                    }
-                    _converse.connection.sendIQ(iq, resolve, reject);
-                });
+                const iq = $iq({to: this.get('jid'), type: "set"})
+                    .c("query", {xmlns: Strophe.NS.MUC_ADMIN})
+                    .c("item", {
+                        'affiliation': member.affiliation || affiliation,
+                        'nick': member.nick,
+                        'jid': member.jid
+                    });
+                if (!_.isUndefined(member.reason)) {
+                    iq.c("reason", member.reason);
+                }
+                return _converse.api.sendIQ(iq);
             },
 
             setAffiliations (members) {

+ 43 - 46
src/headless/converse-roster.js

@@ -329,7 +329,7 @@ converse.plugins.add('converse-roster', {
                 const iq = $iq({type: 'set'})
                     .c('query', {xmlns: Strophe.NS.ROSTER})
                     .c('item', {jid: this.get('jid'), subscription: "remove"});
-                _converse.connection.sendIQ(iq, callback, errback);
+                _converse.api.sendIQ(iq).then(callback).catch(errback);
                 return this;
             }
         });
@@ -451,7 +451,7 @@ converse.plugins.add('converse-roster', {
                 this.addContactToRoster(jid, name, groups, attributes).then(handler, handler);
             },
 
-            sendContactAddIQ (jid, name, groups, callback, errback) {
+            sendContactAddIQ (jid, name, groups) {
                 /*  Send an IQ stanza to the XMPP server to add a new roster contact.
                  *
                  *  Parameters:
@@ -462,14 +462,14 @@ converse.plugins.add('converse-roster', {
                  *    (Function) errback - A function to call if an error occurred
                  */
                 name = _.isEmpty(name)? jid: name;
-                const iq = $iq({type: 'set'})
-                    .c('query', {xmlns: Strophe.NS.ROSTER})
+                const iq = $iq({'type': 'set'})
+                    .c('query', {'xmlns': Strophe.NS.ROSTER})
                     .c('item', { jid, name });
-                _.each(groups, function (group) { iq.c('group').t(group).up(); });
-                _converse.connection.sendIQ(iq, callback, errback);
+                _.each(groups, group => iq.c('group').t(group).up());
+                _converse.api.sendIQ(iq);
             },
 
-            addContactToRoster (jid, name, groups, attributes) {
+            async addContactToRoster (jid, name, groups, attributes) {
                 /* Adds a RosterContact instance to _converse.roster and
                  * registers the contact on the XMPP server.
                  * Returns a promise which is resolved once the XMPP server has
@@ -481,27 +481,22 @@ converse.plugins.add('converse-roster', {
                  *    (Array of Strings) groups - Any roster groups the user might belong to
                  *    (Object) attributes - Any additional attributes to be stored on the user's model.
                  */
-                return new Promise((resolve, reject) => {
-                    groups = groups || [];
-                    this.sendContactAddIQ(jid, name, groups,
-                        () => {
-                            const contact = this.create(_.assignIn({
-                                'ask': undefined,
-                                'nickname': name,
-                                groups,
-                                jid,
-                                'requesting': false,
-                                'subscription': 'none'
-                            }, attributes), {sort: false});
-                            resolve(contact);
-                        },
-                        function (err) {
-                            alert(__('Sorry, there was an error while trying to add %1$s as a contact.', name));
-                            _converse.log(err, Strophe.LogLevel.ERROR);
-                            resolve(err);
-                        }
-                    );
-                });
+                groups = groups || [];
+                try {
+                    await this.sendContactAddIQ(jid, name, groups);
+                } catch (e) {
+                    _converse.log(e, Strophe.LogLevel.ERROR);
+                    alert(__('Sorry, there was an error while trying to add %1$s as a contact.', name));
+                    return e;
+                }
+                return this.create(_.assignIn({
+                    'ask': undefined,
+                    'nickname': name,
+                    groups,
+                    jid,
+                    'requesting': false,
+                    'subscription': 'none'
+                }, attributes), {'sort': false});
             },
 
             subscribeBack (bare_jid, presence) {
@@ -570,24 +565,26 @@ converse.plugins.add('converse-roster', {
                 return _converse.api.disco.stream.getFeature('ver', 'urn:xmpp:features:rosterver') && this.data.get('version');
             },
 
-            fetchFromServer () {
+            async fetchFromServer () {
                 /* Fetch the roster from the XMPP server */
-                return new Promise((resolve, reject) => {
-                    const iq = $iq({
-                        'type': 'get',
-                        'id': _converse.connection.getUniqueId('roster')
-                    }).c('query', {xmlns: Strophe.NS.ROSTER});
-                    if (this.rosterVersioningSupported()) {
-                        iq.attrs({'ver': this.data.get('version')});
-                    }
-                    const callback = _.flow(this.onReceivedFromServer.bind(this), resolve);
-                    const errback = function (iq) {
-                        const errmsg = "Error while trying to fetch roster from the server";
-                        _converse.log(errmsg, Strophe.LogLevel.ERROR);
-                        reject(new Error(errmsg));
-                    }
-                    return _converse.connection.sendIQ(iq, callback, errback);
-                });
+                const stanza = $iq({
+                    'type': 'get',
+                    'id': _converse.connection.getUniqueId('roster')
+                }).c('query', {xmlns: Strophe.NS.ROSTER});
+                if (this.rosterVersioningSupported()) {
+                    stanza.attrs({'ver': this.data.get('version')});
+                }
+                let iq;
+                try {
+                    iq = await _converse.api.sendIQ(stanza);
+                } catch (e) {
+                    _converse.log(e, Strophe.LogLevel.ERROR);
+                    return _converse.log(
+                        "Error while trying to fetch roster from the server",
+                        Strophe.LogLevel.ERROR
+                    );
+                }
+                return this.onReceivedFromServer(iq);
             },
 
             onReceivedFromServer (iq) {
@@ -646,7 +643,7 @@ converse.plugins.add('converse-roster', {
 
             createRequestingContact (presence) {
                 const bare_jid = Strophe.getBareJidFromJid(presence.getAttribute('from')),
-                    nickname = _.get(sizzle(`nick[xmlns="${Strophe.NS.NICK}"]`, presence).pop(), 'textContent', null);
+                      nickname = _.get(sizzle(`nick[xmlns="${Strophe.NS.NICK}"]`, presence).pop(), 'textContent', null);
                 const user_data = {
                     'jid': bare_jid,
                     'subscription': 'none',

+ 16 - 27
src/headless/converse-vcard.js

@@ -56,7 +56,7 @@ converse.plugins.add('converse-vcard', {
         });
 
 
-        function onVCardData (jid, iq, callback) {
+        async function onVCardData (jid, iq) {
             const vcard = iq.querySelector('vCard');
             let result = {};
             if (!_.isNull(vcard)) {
@@ -75,24 +75,10 @@ converse.plugins.add('converse-vcard', {
             }
             if (result.image) {
                 const buffer = u.base64ToArrayBuffer(result['image']);
-                crypto.subtle.digest('SHA-1', buffer)
-                .then(ab => {
-                    result['image_hash'] = u.arrayBufferToHex(ab);
-                    if (callback) callback(result);
-                });
-            } else {
-                if (callback) callback(result);
-            }
-        }
-
-        function onVCardError (jid, iq, errback) {
-            if (errback) {
-                errback({
-                    'stanza': iq,
-                    'jid': jid,
-                    'vcard_error': moment().format()
-                });
+                const ab = await crypto.subtle.digest('SHA-1', buffer);
+                result['image_hash'] = u.arrayBufferToHex(ab);
             }
+            return result;
         }
 
         function createStanza (type, jid, vcard_el) {
@@ -113,7 +99,7 @@ converse.plugins.add('converse-vcard', {
             return _converse.api.sendIQ(createStanza("set", jid, vcard_el));
         }
 
-        function getVCard (_converse, jid) {
+        async function getVCard (_converse, jid) {
             /* Request the VCard of another user. Returns a promise.
              *
              * Parameters:
@@ -121,14 +107,17 @@ converse.plugins.add('converse-vcard', {
              *      is being requested.
              */
             const to = Strophe.getBareJidFromJid(jid) === _converse.bare_jid ? null : jid;
-            return new Promise((resolve, reject) => {
-                _converse.connection.sendIQ(
-                    createStanza("get", to),
-                    _.partial(onVCardData, jid, _, resolve),
-                    _.partial(onVCardError, jid, _, resolve),
-                    _converse.IQ_TIMEOUT
-                );
-            });
+            let iq;
+            try {
+                iq = await _converse.api.sendIQ(createStanza("get", to))
+            } catch (iq) {
+                return {
+                    'stanza': iq,
+                    'jid': jid,
+                    'vcard_error': moment().format()
+                }
+            }
+            return onVCardData(jid, iq);
         }
 
         /* Event handlers */