Pārlūkot izejas kodu

Fix failing tests

JC Brand 7 gadi atpakaļ
vecāks
revīzija
fc94127014
4 mainītis faili ar 475 papildinājumiem un 410 dzēšanām
  1. 161 130
      spec/chatbox.js
  2. 10 2
      spec/chatroom.js
  3. 283 277
      spec/protocol.js
  4. 21 1
      tests/utils.js

+ 161 - 130
spec/chatbox.js

@@ -10,6 +10,7 @@
 } (this, function ($, jasmine, utils, converse, mock, test_utils) {
 } (this, function ($, jasmine, utils, converse, mock, test_utils) {
     "use strict";
     "use strict";
     var _ = converse.env._;
     var _ = converse.env._;
+    var $iq = converse.env.$iq;
     var $msg = converse.env.$msg;
     var $msg = converse.env.$msg;
     var Strophe = converse.env.Strophe;
     var Strophe = converse.env.Strophe;
     var moment = converse.env.moment;
     var moment = converse.env.moment;
@@ -22,30 +23,37 @@
                     null, ['rosterGroupsFetched'], {},
                     null, ['rosterGroupsFetched'], {},
                     function (done, _converse) {
                     function (done, _converse) {
 
 
-                test_utils.createContacts(_converse, 'current');
-                test_utils.openControlBox();
-                test_utils.openContactsPanel(_converse);
-                expect(_converse.chatboxes.length).toEqual(1);
-                var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
-                var message = '/me is tired';
-                var msg = $msg({
-                        from: sender_jid,
-                        to: _converse.connection.jid,
-                        type: 'chat',
-                        id: (new Date()).getTime()
-                    }).c('body').t(message).up()
-                    .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
+                test_utils.waitUntilFeatureSupportConfirmed(_converse, 'vcard-temp')
+                .then(function () {
+                    return test_utils.waitUntil(function () {
+                        return _converse.xmppstatus.get('fullname');
+                    }, 300);
+                }).then(function () {
+                    test_utils.createContacts(_converse, 'current');
+                    test_utils.openControlBox();
+                    test_utils.openContactsPanel(_converse);
+                    expect(_converse.chatboxes.length).toEqual(1);
+                    var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+                    var message = '/me is tired';
+                    var msg = $msg({
+                            from: sender_jid,
+                            to: _converse.connection.jid,
+                            type: 'chat',
+                            id: (new Date()).getTime()
+                        }).c('body').t(message).up()
+                        .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
 
 
-                _converse.chatboxes.onMessage(msg);
-                var view = _converse.chatboxviews.get(sender_jid);
-                expect(_.includes(view.$el.find('.chat-msg-author').text(), '**Max Frankfurter')).toBeTruthy();
-                expect(view.$el.find('.chat-msg-content').text()).toBe(' is tired');
+                    _converse.chatboxes.onMessage(msg);
+                    var view = _converse.chatboxviews.get(sender_jid);
+                    expect(_.includes(view.$el.find('.chat-msg-author').text(), '**Max Frankfurter')).toBeTruthy();
+                    expect(view.$el.find('.chat-msg-content').text()).toBe(' is tired');
 
 
-                message = '/me is as well';
-                test_utils.sendMessage(view, message);
-                expect(_.includes(view.$el.find('.chat-msg-author:last').text(), '**Max Mustermann')).toBeTruthy();
-                expect(view.$el.find('.chat-msg-content:last').text()).toBe(' is as well');
-                done();
+                    message = '/me is as well';
+                    test_utils.sendMessage(view, message);
+                    expect(_.includes(view.$el.find('.chat-msg-author:last').text(), '**Max Mustermann')).toBeTruthy();
+                    expect(view.$el.find('.chat-msg-content:last').text()).toBe(' is as well');
+                    done();
+                });
             }));
             }));
 
 
             it("is created when you click on a roster item",
             it("is created when you click on a roster item",
@@ -527,7 +535,8 @@
             describe("A Chat Message", function () {
             describe("A Chat Message", function () {
 
 
                 describe("when received from someone else", function () {
                 describe("when received from someone else", function () {
-                    it("can be received which will open a chatbox and be displayed inside it",
+
+                    it("will open a chatbox and be displayed inside it",
                         mock.initConverseWithPromises(
                         mock.initConverseWithPromises(
                             null, ['rosterGroupsFetched'], {},
                             null, ['rosterGroupsFetched'], {},
                             function (done, _converse) {
                             function (done, _converse) {
@@ -953,47 +962,55 @@
                         null, ['rosterGroupsFetched'], {},
                         null, ['rosterGroupsFetched'], {},
                         function (done, _converse) {
                         function (done, _converse) {
 
 
-                    test_utils.createContacts(_converse, 'current');
-                    test_utils.openControlBox();
-                    test_utils.openContactsPanel(_converse);
+                    var contact, sent_stanza, IQ_id, stanza;
+                    test_utils.waitUntilFeatureSupportConfirmed(_converse, 'vcard-temp')
+                    .then(function () {
+                        return test_utils.waitUntil(function () {
+                            return _converse.xmppstatus.get('fullname');
+                        }, 300);
+                    }).then(function () {
+                        test_utils.createContacts(_converse, 'current');
+                        test_utils.openControlBox();
+                        test_utils.openContactsPanel(_converse);
 
 
-                    // Send a message from a different resource
-                    spyOn(_converse, 'log');
-                    var msgtext = 'This is a sent carbon message';
-                    var recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@localhost';
-                    var msg = $msg({
-                            'from': _converse.bare_jid,
-                            'id': (new Date()).getTime(),
-                            'to': _converse.connection.jid,
-                            'type': 'chat',
-                            'xmlns': 'jabber:client'
-                        }).c('sent', {'xmlns': 'urn:xmpp:carbons:2'})
-                          .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
-                          .c('message', {
-                                'xmlns': 'jabber:client',
-                                'from': _converse.bare_jid+'/another-resource',
-                                'to': recipient_jid,
-                                'type': 'chat'
-                        }).c('body').t(msgtext).tree();
-                    _converse.chatboxes.onMessage(msg);
+                        // Send a message from a different resource
+                        spyOn(_converse, 'log');
+                        var msgtext = 'This is a sent carbon message';
+                        var recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@localhost';
+                        var msg = $msg({
+                                'from': _converse.bare_jid,
+                                'id': (new Date()).getTime(),
+                                'to': _converse.connection.jid,
+                                'type': 'chat',
+                                'xmlns': 'jabber:client'
+                            }).c('sent', {'xmlns': 'urn:xmpp:carbons:2'})
+                            .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
+                            .c('message', {
+                                    'xmlns': 'jabber:client',
+                                    'from': _converse.bare_jid+'/another-resource',
+                                    'to': recipient_jid,
+                                    'type': 'chat'
+                            }).c('body').t(msgtext).tree();
+                        _converse.chatboxes.onMessage(msg);
 
 
-                    // Check that the chatbox and its view now exist
-                    var chatbox = _converse.chatboxes.get(recipient_jid);
-                    var chatboxview = _converse.chatboxviews.get(recipient_jid);
-                    expect(chatbox).toBeDefined();
-                    expect(chatboxview).toBeDefined();
-                    // Check that the message was received and check the message parameters
-                    expect(chatbox.messages.length).toEqual(1);
-                    var msg_obj = chatbox.messages.models[0];
-                    expect(msg_obj.get('message')).toEqual(msgtext);
-                    expect(msg_obj.get('fullname')).toEqual(_converse.xmppstatus.get('fullname'));
-                    expect(msg_obj.get('sender')).toEqual('me');
-                    expect(msg_obj.get('delayed')).toEqual(false);
-                    // Now check that the message appears inside the chatbox in the DOM
-                    var $chat_content = chatboxview.$el.find('.chat-content');
-                    var msg_txt = $chat_content.find('.chat-message').find('.chat-msg-content').text();
-                    expect(msg_txt).toEqual(msgtext);
-                    done();
+                        // Check that the chatbox and its view now exist
+                        var chatbox = _converse.chatboxes.get(recipient_jid);
+                        var chatboxview = _converse.chatboxviews.get(recipient_jid);
+                        expect(chatbox).toBeDefined();
+                        expect(chatboxview).toBeDefined();
+                        // Check that the message was received and check the message parameters
+                        expect(chatbox.messages.length).toEqual(1);
+                        var msg_obj = chatbox.messages.models[0];
+                        expect(msg_obj.get('message')).toEqual(msgtext);
+                        expect(msg_obj.get('fullname')).toEqual(_converse.xmppstatus.get('fullname'));
+                        expect(msg_obj.get('sender')).toEqual('me');
+                        expect(msg_obj.get('delayed')).toEqual(false);
+                        // Now check that the message appears inside the chatbox in the DOM
+                        var $chat_content = chatboxview.$el.find('.chat-content');
+                        var msg_txt = $chat_content.find('.chat-message').find('.chat-msg-content').text();
+                        expect(msg_txt).toEqual(msgtext);
+                        done();
+                    });
                 }));
                 }));
 
 
                 it("will be discarded if it's a malicious message meant to look like a carbon copy",
                 it("will be discarded if it's a malicious message meant to look like a carbon copy",
@@ -1561,41 +1578,48 @@
                             null, ['rosterGroupsFetched'], {},
                             null, ['rosterGroupsFetched'], {},
                             function (done, _converse) {
                             function (done, _converse) {
 
 
-                        test_utils.createContacts(_converse, 'current');
-
-                        // Send a message from a different resource
-                        spyOn(_converse, 'log');
-                        var recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@localhost';
-                        test_utils.openChatBoxFor(_converse, recipient_jid);
-                        var msg = $msg({
-                                'from': _converse.bare_jid,
-                                'id': (new Date()).getTime(),
-                                'to': _converse.connection.jid,
-                                'type': 'chat',
-                                'xmlns': 'jabber:client'
-                            }).c('sent', {'xmlns': 'urn:xmpp:carbons:2'})
-                              .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
-                              .c('message', {
-                                    'xmlns': 'jabber:client',
-                                    'from': _converse.bare_jid+'/another-resource',
-                                    'to': recipient_jid,
-                                    'type': 'chat'
-                            }).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                        _converse.chatboxes.onMessage(msg);
+                        var contact, sent_stanza, IQ_id, stanza;
+                        test_utils.waitUntilFeatureSupportConfirmed(_converse, 'vcard-temp')
+                        .then(function () {
+                            return test_utils.waitUntil(function () {
+                                return _converse.xmppstatus.get('fullname');
+                            }, 300);
+                        }).then(function () {
+                            test_utils.createContacts(_converse, 'current');
+                            // Send a message from a different resource
+                            spyOn(_converse, 'log');
+                            var recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@localhost';
+                            test_utils.openChatBoxFor(_converse, recipient_jid);
+                            var msg = $msg({
+                                    'from': _converse.bare_jid,
+                                    'id': (new Date()).getTime(),
+                                    'to': _converse.connection.jid,
+                                    'type': 'chat',
+                                    'xmlns': 'jabber:client'
+                                }).c('sent', {'xmlns': 'urn:xmpp:carbons:2'})
+                                  .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
+                                  .c('message', {
+                                        'xmlns': 'jabber:client',
+                                        'from': _converse.bare_jid+'/another-resource',
+                                        'to': recipient_jid,
+                                        'type': 'chat'
+                                }).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
+                            _converse.chatboxes.onMessage(msg);
 
 
-                        // Check that the chatbox and its view now exist
-                        var chatbox = _converse.chatboxes.get(recipient_jid);
-                        var chatboxview = _converse.chatboxviews.get(recipient_jid);
-                        // Check that the message was received and check the message parameters
-                        expect(chatbox.messages.length).toEqual(1);
-                        var msg_obj = chatbox.messages.models[0];
-                        expect(msg_obj.get('fullname')).toEqual(_converse.xmppstatus.get('fullname'));
-                        expect(msg_obj.get('sender')).toEqual('me');
-                        expect(msg_obj.get('delayed')).toEqual(false);
-                        var $chat_content = chatboxview.$el.find('.chat-content');
-                        var status_text = $chat_content.find('.chat-info.chat-event').text();
-                        expect(status_text).toBe('Typing from another device');
-                        done();
+                            // Check that the chatbox and its view now exist
+                            var chatbox = _converse.chatboxes.get(recipient_jid);
+                            var chatboxview = _converse.chatboxviews.get(recipient_jid);
+                            // Check that the message was received and check the message parameters
+                            expect(chatbox.messages.length).toEqual(1);
+                            var msg_obj = chatbox.messages.models[0];
+                            expect(msg_obj.get('fullname')).toEqual(_converse.xmppstatus.get('fullname'));
+                            expect(msg_obj.get('sender')).toEqual('me');
+                            expect(msg_obj.get('delayed')).toEqual(false);
+                            var $chat_content = chatboxview.$el.find('.chat-content');
+                            var status_text = $chat_content.find('.chat-info.chat-event').text();
+                            expect(status_text).toBe('Typing from another device');
+                            done();
+                        });
                     }));
                     }));
                 });
                 });
 
 
@@ -1702,41 +1726,48 @@
                             null, ['rosterGroupsFetched'], {},
                             null, ['rosterGroupsFetched'], {},
                             function (done, _converse) {
                             function (done, _converse) {
 
 
-                        test_utils.createContacts(_converse, 'current');
-
-                        // Send a message from a different resource
-                        spyOn(_converse, 'log');
-                        var recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@localhost';
-                        test_utils.openChatBoxFor(_converse, recipient_jid);
-                        var msg = $msg({
-                                'from': _converse.bare_jid,
-                                'id': (new Date()).getTime(),
-                                'to': _converse.connection.jid,
-                                'type': 'chat',
-                                'xmlns': 'jabber:client'
-                            }).c('sent', {'xmlns': 'urn:xmpp:carbons:2'})
-                              .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
-                              .c('message', {
-                                    'xmlns': 'jabber:client',
-                                    'from': _converse.bare_jid+'/another-resource',
-                                    'to': recipient_jid,
-                                    'type': 'chat'
-                            }).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                        _converse.chatboxes.onMessage(msg);
+                        var contact, sent_stanza, IQ_id, stanza;
+                        test_utils.waitUntilFeatureSupportConfirmed(_converse, 'vcard-temp')
+                        .then(function () {
+                            return test_utils.waitUntil(function () {
+                                return _converse.xmppstatus.get('fullname');
+                            }, 300);
+                        }).then(function () {
+                            test_utils.createContacts(_converse, 'current');
+                            // Send a message from a different resource
+                            spyOn(_converse, 'log');
+                            var recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@localhost';
+                            test_utils.openChatBoxFor(_converse, recipient_jid);
+                            var msg = $msg({
+                                    'from': _converse.bare_jid,
+                                    'id': (new Date()).getTime(),
+                                    'to': _converse.connection.jid,
+                                    'type': 'chat',
+                                    'xmlns': 'jabber:client'
+                                }).c('sent', {'xmlns': 'urn:xmpp:carbons:2'})
+                                  .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
+                                  .c('message', {
+                                        'xmlns': 'jabber:client',
+                                        'from': _converse.bare_jid+'/another-resource',
+                                        'to': recipient_jid,
+                                        'type': 'chat'
+                                }).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
+                            _converse.chatboxes.onMessage(msg);
 
 
-                        // Check that the chatbox and its view now exist
-                        var chatbox = _converse.chatboxes.get(recipient_jid);
-                        var chatboxview = _converse.chatboxviews.get(recipient_jid);
-                        // Check that the message was received and check the message parameters
-                        expect(chatbox.messages.length).toEqual(1);
-                        var msg_obj = chatbox.messages.models[0];
-                        expect(msg_obj.get('fullname')).toEqual(_converse.xmppstatus.get('fullname'));
-                        expect(msg_obj.get('sender')).toEqual('me');
-                        expect(msg_obj.get('delayed')).toEqual(false);
-                        var $chat_content = chatboxview.$el.find('.chat-content');
-                        var status_text = $chat_content.find('.chat-info.chat-event').text();
-                        expect(status_text).toBe('Stopped typing on the other device');
-                        done();
+                            // Check that the chatbox and its view now exist
+                            var chatbox = _converse.chatboxes.get(recipient_jid);
+                            var chatboxview = _converse.chatboxviews.get(recipient_jid);
+                            // Check that the message was received and check the message parameters
+                            expect(chatbox.messages.length).toEqual(1);
+                            var msg_obj = chatbox.messages.models[0];
+                            expect(msg_obj.get('fullname')).toEqual(_converse.xmppstatus.get('fullname'));
+                            expect(msg_obj.get('sender')).toEqual('me');
+                            expect(msg_obj.get('delayed')).toEqual(false);
+                            var $chat_content = chatboxview.$el.find('.chat-content');
+                            var status_text = $chat_content.find('.chat-info.chat-event').text();
+                            expect(status_text).toBe('Stopped typing on the other device');
+                            done();
+                        });
                     }));
                     }));
                 });
                 });
 
 

+ 10 - 2
spec/chatroom.js

@@ -546,8 +546,16 @@
                     null, ['rosterGroupsFetched'], {},
                     null, ['rosterGroupsFetched'], {},
                     function (done, _converse) {
                     function (done, _converse) {
 
 
-                test_utils.createContacts(_converse, 'current');
-                test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy').then(function () {
+
+                test_utils.waitUntilFeatureSupportConfirmed(_converse, 'vcard-temp')
+                .then(function () {
+                    return test_utils.waitUntil(function () {
+                        return _converse.xmppstatus.get('fullname');
+                    }, 300);
+                }).then(function () {
+                    test_utils.createContacts(_converse, 'current');
+                    return test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy');
+                }).then(function () {
                     var view = _converse.chatboxviews.get('lounge@localhost');
                     var view = _converse.chatboxviews.get('lounge@localhost');
                     if (!view.$el.find('.chat-area').length) { view.renderChatArea(); }
                     if (!view.$el.find('.chat-area').length) { view.renderChatArea(); }
                     var message = '/me is tired';
                     var message = '/me is tired';

+ 283 - 277
spec/protocol.js

@@ -54,178 +54,185 @@
                     { roster_groups: false },
                     { roster_groups: false },
                     function (done, _converse) {
                     function (done, _converse) {
 
 
-                /* The process by which a user subscribes to a contact, including
-                * the interaction between roster items and subscription states.
-                */
-                var contact, stanza, sent_stanza, IQ_id;
-                test_utils.openControlBox(_converse);
-                var panel = _converse.chatboxviews.get('controlbox').contactspanel;
-                spyOn(panel, "addContactFromForm").and.callThrough();
-                spyOn(_converse.roster, "addAndSubscribe").and.callThrough();
-                spyOn(_converse.roster, "addContact").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);
-                });
-                panel.delegateEvents(); // Rebind all events so that our spy gets called
-
-                /* Add a new contact through the UI */
-                var form = panel.el.querySelector('form.add-xmpp-contact');
-                expect(_.isNull(form)).toBeTruthy();
-
-                // Click the "Add a contact" link.
-                panel.$('.toggle-xmpp-contact-form').click();
-
-                // Check that the form appears
-                form = panel.el.querySelector('form.add-xmpp-contact');
-                expect(form.parentElement.offsetHeight).not.toBe(0);
-                expect(_.includes(form.parentElement.classList, 'collapsed')).toBeFalsy();
-
-                // Fill in the form and submit
-                $(form).find('input').val('contact@example.org');
-                $(form).submit();
-
-                /* 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(panel.addContactFromForm).toHaveBeenCalled();
-                expect(_converse.roster.addAndSubscribe).toHaveBeenCalled();
-                expect(_converse.roster.addContact).toHaveBeenCalled();
-
-                // The form should not be visible anymore (by virtue of its
-                // parent being collapsed)
-                expect(form.parentElement.offsetHeight).toBe(0);
-                expect(_.includes(form.parentElement.classList, 'collapsed')).toBeTrue;
-
-                /* _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 type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
-                        "<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));
+                var contact, sent_stanza, IQ_id, stanza;
+                test_utils.waitUntilFeatureSupportConfirmed(_converse, 'vcard-temp')
+                .then(function () {
+                    return test_utils.waitUntil(function () {
+                        return _converse.xmppstatus.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 panel = _converse.chatboxviews.get('controlbox').contactspanel;
+                    spyOn(panel, "addContactFromForm").and.callThrough();
+                    spyOn(_converse.roster, "addAndSubscribe").and.callThrough();
+                    spyOn(_converse.roster, "addContact").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);
+                    });
+                    panel.delegateEvents(); // Rebind all events so that our spy gets called
 
 
-                // 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();
+                    /* Add a new contact through the UI */
+                    var form = panel.el.querySelector('form.add-xmpp-contact');
+                    expect(_.isNull(form)).toBeTruthy();
 
 
-                /* 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'/>
-                */
+                    // Click the "Add a contact" link.
+                    panel.$('.toggle-xmpp-contact-form').click();
 
 
-                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': 'dummy@localhost'})
-                    .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.
-                test_utils.waitUntil(function () {
-                    return $('a:contains("Pending contacts")').length;
-                }, 300).then(function () {
+                    // Check that the form appears
+                    form = panel.el.querySelector('form.add-xmpp-contact');
+                    expect(form.parentElement.offsetHeight).not.toBe(0);
+                    expect(_.includes(form.parentElement.classList, 'collapsed')).toBeFalsy();
+
+                    // Fill in the form and submit
+                    $(form).find('input').val('contact@example.org');
+                    $(form).submit();
+
+                    /* 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(panel.addContactFromForm).toHaveBeenCalled();
+                    expect(_converse.roster.addAndSubscribe).toHaveBeenCalled();
+                    expect(_converse.roster.addContact).toHaveBeenCalled();
+
+                    // The form should not be visible anymore (by virtue of its
+                    // parent being collapsed)
+                    expect(form.parentElement.offsetHeight).toBe(0);
+                    expect(_.includes(form.parentElement.classList, 'collapsed')).toBeTrue;
+
+                    /* _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 type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                            "<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': 'dummy@localhost'})
+                        .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 () {
+                        return $('a:contains("Pending contacts")').length;
+                    }, 300);
+                }).then(function () {
                     var $header = $('a:contains("Pending contacts")');
                     var $header = $('a:contains("Pending contacts")');
                     expect($header.length).toBe(1);
                     expect($header.length).toBe(1);
                     expect($header.is(":visible")).toBeTruthy();
                     expect($header.is(":visible")).toBeTruthy();
@@ -234,13 +241,13 @@
 
 
                     spyOn(contact, "ackSubscribe").and.callThrough();
                     spyOn(contact, "ackSubscribe").and.callThrough();
                     /* Here we assume the "happy path" that the contact
                     /* Here we assume the "happy path" that the contact
-                    * approves the subscription request
-                    *
-                    *  <presence
-                    *      to='user@example.com'
-                    *      from='contact@example.org'
-                    *      type='subscribed'/>
-                    */
+                     * approves the subscription request
+                     *
+                     *  <presence
+                     *      to='user@example.com'
+                     *      from='contact@example.org'
+                     *      type='subscribed'/>
+                     */
                     stanza = $pres({
                     stanza = $pres({
                         'to': _converse.bare_jid,
                         'to': _converse.bare_jid,
                         'from': 'contact@example.org',
                         'from': 'contact@example.org',
@@ -249,31 +256,31 @@
                     sent_stanza = ""; // Reset
                     sent_stanza = ""; // Reset
                     _converse.connection._dataRecv(test_utils.createRequest(stanza));
                     _converse.connection._dataRecv(test_utils.createRequest(stanza));
                     /* Upon receiving the presence stanza of type "subscribed",
                     /* 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".
-                    */
+                     * the user SHOULD acknowledge receipt of that
+                     * subscription state notification by sending a presence
+                     * stanza of type "subscribe".
+                     */
                     expect(contact.ackSubscribe).toHaveBeenCalled();
                     expect(contact.ackSubscribe).toHaveBeenCalled();
                     expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec)
                     expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec)
                         "<presence type='subscribe' to='contact@example.org' xmlns='jabber:client'/>"
                         "<presence type='subscribe' to='contact@example.org' xmlns='jabber:client'/>"
                     );
                     );
 
 
                     /* The user's server MUST initiate a roster push to all of the user's
                     /* 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>
-                    */
+                     * 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');
                     IQ_id = _converse.connection.getUniqueId('roster');
                     stanza = $iq({'type': 'set', 'id': IQ_id})
                     stanza = $iq({'type': 'set', 'id': IQ_id})
                         .c('query', {'xmlns': 'jabber:iq:roster'})
                         .c('query', {'xmlns': 'jabber:iq:roster'})
@@ -303,22 +310,22 @@
                     expect(contact.get('chat_status')).toBe('offline');
                     expect(contact.get('chat_status')).toBe('offline');
 
 
                     /*  <presence
                     /*  <presence
-                    *      from='contact@example.org/resource'
-                    *      to='user@example.com/resource'/>
-                    */
+                     *      from='contact@example.org/resource'
+                     *      to='user@example.com/resource'/>
+                     */
                     stanza = $pres({'to': _converse.bare_jid, 'from': 'contact@example.org/resource'});
                     stanza = $pres({'to': _converse.bare_jid, 'from': 'contact@example.org/resource'});
                     _converse.connection._dataRecv(test_utils.createRequest(stanza));
                     _converse.connection._dataRecv(test_utils.createRequest(stanza));
                     // Now the contact should also be online.
                     // Now the contact should also be online.
                     expect(contact.get('chat_status')).toBe('online');
                     expect(contact.get('chat_status')).toBe('online');
 
 
                     /* Section 8.3.  Creating a Mutual Subscription
                     /* 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'/>
-                    */
+                     *
+                     * 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(contact, 'authorize').and.callThrough();
                     spyOn(_converse.roster, 'handleIncomingSubscription').and.callThrough();
                     spyOn(_converse.roster, 'handleIncomingSubscription').and.callThrough();
                     stanza = $pres({
                     stanza = $pres({
@@ -329,32 +336,32 @@
                     expect(_converse.roster.handleIncomingSubscription).toHaveBeenCalled();
                     expect(_converse.roster.handleIncomingSubscription).toHaveBeenCalled();
 
 
                     /* The user's client MUST send a presence stanza of type
                     /* 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'/>
-                    */
+                     * "subscribed" to the contact in order to approve the
+                     * subscription request.
+                     *
+                     *  <presence to='contact@example.org' type='subscribed'/>
+                     */
                     expect(contact.authorize).toHaveBeenCalled();
                     expect(contact.authorize).toHaveBeenCalled();
                     expect(sent_stanza.toLocaleString()).toBe(
                     expect(sent_stanza.toLocaleString()).toBe(
                         "<presence to='contact@example.org' type='subscribed' xmlns='jabber:client'/>"
                         "<presence to='contact@example.org' type='subscribed' xmlns='jabber:client'/>"
                     );
                     );
 
 
                     /* As a result, the user's server MUST initiate a
                     /* 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>
-                    */
+                     * 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'})
                     stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'})
                         .c('item', {
                         .c('item', {
                             'jid': 'contact@example.org',
                             'jid': 'contact@example.org',
@@ -368,7 +375,6 @@
                     expect($contacts.hasClass('both')).toBeTruthy();
                     expect($contacts.hasClass('both')).toBeTruthy();
                     done();
                     done();
                 });
                 });
-                });
             }));
             }));
 
 
             it("Alternate Flow: Contact Declines Subscription Request",
             it("Alternate Flow: Contact Declines Subscription Request",
@@ -377,8 +383,8 @@
                     function (done, _converse) {
                     function (done, _converse) {
 
 
                 /* The process by which a user subscribes to a contact, including
                 /* The process by which a user subscribes to a contact, including
-                * the interaction between roster items and subscription states.
-                */
+                 * the interaction between roster items and subscription states.
+                 */
                 var contact, stanza, sent_stanza, sent_IQ;
                 var contact, stanza, sent_stanza, sent_IQ;
                 test_utils.openControlBox(_converse);
                 test_utils.openControlBox(_converse);
                 // Add a new roster contact via roster push
                 // Add a new roster contact via roster push
@@ -401,32 +407,32 @@
                     sent_IQ = iq;
                     sent_IQ = iq;
                 });
                 });
                 /* We now assume the contact declines the subscription
                 /* We now assume the contact declines the subscription
-                * requests.
-                *
-                /* Upon receiving the presence stanza of type "unsubscribed"
-                * addressed to the user, the user's server (1) MUST deliver
-                * that presence stanza to the user and (2) 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 "none" and with no 'ask' attribute:
-                *
-                *  <presence
-                *      from='contact@example.org'
-                *      to='user@example.com'
-                *      type='unsubscribed'/>
-                *
-                *  <iq type='set'>
-                *  <query xmlns='jabber:iq:roster'>
-                *      <item
-                *          jid='contact@example.org'
-                *          subscription='none'
-                *          name='MyContact'>
-                *      <group>MyBuddies</group>
-                *      </item>
-                *  </query>
-                *  </iq>
-                */
+                 * requests.
+                 *
+                 * Upon receiving the presence stanza of type "unsubscribed"
+                 * addressed to the user, the user's server (1) MUST deliver
+                 * that presence stanza to the user and (2) 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 "none" and with no 'ask' attribute:
+                 *
+                 *  <presence
+                 *      from='contact@example.org'
+                 *      to='user@example.com'
+                 *      type='unsubscribed'/>
+                 *
+                 *  <iq type='set'>
+                 *  <query xmlns='jabber:iq:roster'>
+                 *      <item
+                 *          jid='contact@example.org'
+                 *          subscription='none'
+                 *          name='MyContact'>
+                 *      <group>MyBuddies</group>
+                 *      </item>
+                 *  </query>
+                 *  </iq>
+                 */
                 // FIXME: also add the <iq>
                 // FIXME: also add the <iq>
                 stanza = $pres({
                 stanza = $pres({
                     'to': _converse.bare_jid,
                     'to': _converse.bare_jid,
@@ -436,18 +442,18 @@
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
 
 
                 /* Upon receiving the presence stanza of type "unsubscribed",
                 /* Upon receiving the presence stanza of type "unsubscribed",
-                * the user SHOULD acknowledge receipt of that subscription
-                * state notification through either "affirming" it by
-                * sending a presence stanza of type "unsubscribe
-                */
+                 * the user SHOULD acknowledge receipt of that subscription
+                 * state notification through either "affirming" it by
+                 * sending a presence stanza of type "unsubscribe
+                 */
                 expect(contact.ackUnsubscribe).toHaveBeenCalled();
                 expect(contact.ackUnsubscribe).toHaveBeenCalled();
                 expect(sent_stanza.toLocaleString()).toBe(
                 expect(sent_stanza.toLocaleString()).toBe(
                     "<presence type='unsubscribe' to='contact@example.org' xmlns='jabber:client'/>"
                     "<presence type='unsubscribe' to='contact@example.org' xmlns='jabber:client'/>"
                 );
                 );
 
 
                 /* _converse.js will then also automatically remove the
                 /* _converse.js will then also automatically remove the
-                * contact from the user's roster.
-                */
+                 * contact from the user's roster.
+                 */
                 expect(sent_IQ.toLocaleString()).toBe(
                 expect(sent_IQ.toLocaleString()).toBe(
                     "<iq type='set' xmlns='jabber:client'>"+
                     "<iq type='set' xmlns='jabber:client'>"+
                         "<query xmlns='jabber:iq:roster'>"+
                         "<query xmlns='jabber:iq:roster'>"+
@@ -486,24 +492,24 @@
                     expect(window.confirm).toHaveBeenCalled();
                     expect(window.confirm).toHaveBeenCalled();
 
 
                     /* Section 8.6 Removing a Roster Item and Cancelling All
                     /* 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>
-                    */
+                     * 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(
                     expect(sent_IQ.toLocaleString()).toBe(
                         "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
                         "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
                             "<query xmlns='jabber:iq:roster'>"+
                             "<query xmlns='jabber:iq:roster'>"+

+ 21 - 1
tests/utils.js

@@ -11,7 +11,27 @@
     if (typeof window.Promise === 'undefined') {
     if (typeof window.Promise === 'undefined') {
         waitUntilPromise.setPromiseImplementation(Promise);
         waitUntilPromise.setPromiseImplementation(Promise);
     }
     }
-    utils.waitUntil = waitUntilPromise['default'];
+    utils.waitUntil = waitUntilPromise.default;
+
+    utils.waitUntilFeatureSupportConfirmed = function (_converse, feature_name) {
+        var IQ_disco, stanza;
+        return utils.waitUntil(function () {
+            IQ_disco = _.filter(_converse.connection.IQ_stanzas, function (iq) {
+                return iq.nodeTree.querySelector('query[xmlns="http://jabber.org/protocol/disco#info"]');
+            }).pop();
+            return !_.isUndefined(IQ_disco);
+        }, 300).then(function () {
+            var info_IQ_id = IQ_disco.nodeTree.getAttribute('id');
+            stanza = $iq({
+                'type': 'result',
+                'from': 'localhost',
+                'to': 'dummy@localhost/resource',
+                'id': info_IQ_id
+            }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
+                .c('feature', {'var': feature_name});
+            _converse.connection._dataRecv(utils.createRequest(stanza));
+        });
+    }
 
 
     utils.createRequest = function (iq) {
     utils.createRequest = function (iq) {
         iq = typeof iq.tree == "function" ? iq.tree() : iq;
         iq = typeof iq.tree == "function" ? iq.tree() : iq;