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

Make the message view's `render` method async

So that we first render dynamic content (e.g. images) before inserting
it into the chat.

Also, add the `show_images_inline` setting (which is the cause of this
whole change).

Updated tests to handle this new change and start using async/await
instead of promise callbacks.
JC Brand 6 жил өмнө
parent
commit
e181aaf99b

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


+ 1 - 3
package-lock.json

@@ -2252,9 +2252,7 @@
       }
     },
     "backbone.browserStorage": {
-      "version": "0.0.3",
-      "resolved": "https://registry.npmjs.org/backbone.browserStorage/-/backbone.browserStorage-0.0.3.tgz",
-      "integrity": "sha1-ikIi8I2bHQslLR14/1CUuNCKc2s=",
+      "version": "github:jcbrand/Backbone.browserStorage#7079bf7bf9a43474da1d48e31e3cda6c4a716382",
       "dev": true,
       "requires": {
         "backbone": "1.3.3",

+ 1 - 1
package.json

@@ -40,7 +40,7 @@
     "awesomplete-avoid-xss": "^1.1.2",
     "babel-loader": "^8.0.0-beta.3",
     "backbone": "1.3.3",
-    "backbone.browserStorage": "0.0.3",
+    "backbone.browserStorage": "jcbrand/Backbone.browserStorage#78e4a58f7cee10cad6af4fd5f3213331a396aa5a",
     "backbone.nativeview": "^0.3.3",
     "backbone.overview": "1.0.2",
     "backbone.vdomview": "1.0.1",

+ 103 - 126
spec/chatbox.js

@@ -58,61 +58,49 @@
             it("supports the /me command",
                 mock.initConverseWithPromises(
                     null, ['rosterGroupsFetched'], {},
-                    function (done, _converse) {
+                    async function (done, _converse) {
 
-                var view;
                 test_utils.createContacts(_converse, 'current');
-                test_utils.waitUntilDiscoConfirmed(_converse, 'localhost', [], ['vcard-temp'])
-                .then(() => test_utils.waitUntil(() => _converse.xmppstatus.vcard.get('fullname')), 300)
-                .then(function () {
-                    test_utils.openControlBox();
-                    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);
-                    view = _converse.chatboxviews.get(sender_jid);
+                await test_utils.waitUntilDiscoConfirmed(_converse, 'localhost', [], ['vcard-temp']);
+                await test_utils.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
+                await test_utils.openControlBox();
+                expect(_converse.chatboxes.length).toEqual(1);
+                const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+                let message = '/me is tired';
+                const 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.waitUntil(function () {
-                        return u.isVisible(view.el);
-                    }).then(function () {
-                        expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(1);
-                        expect(_.includes(view.el.querySelector('.chat-msg__author').textContent, '**Max Frankfurter')).toBeTruthy();
-                        expect($(view.el).find('.chat-msg__text').text()).toBe(' is tired');
-
-                        message = '/me is as well';
-                        test_utils.sendMessage(view, message);
-                        expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(2);
-
-                        return test_utils.waitUntil(() => $(view.el).find('.chat-msg__author:last').text().trim() === '**Max Mustermann');
-                    }).then(function () {
-                        expect($(view.el).find('.chat-msg__text:last').text()).toBe(' is as well');
-                        expect($(view.el).find('.chat-msg:last').hasClass('chat-msg--followup')).toBe(false);
-
-                        // Check that /me messages after a normal message don't
-                        // get the 'chat-msg--followup' class.
-                        message = 'This a normal message';
-                        test_utils.sendMessage(view, message);
-                        let message_el = view.el.querySelector('.message:last-child');
-                        expect(u.hasClass('chat-msg--followup', message_el)).toBeFalsy();
-
-                        message = '/me wrote a 3rd person message';
-                        test_utils.sendMessage(view, message);
-                        message_el = view.el.querySelector('.message:last-child');
-                        expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(3);
-                        expect($(view.el).find('.chat-msg__text:last').text()).toBe(' wrote a 3rd person message');
-                        expect($(view.el).find('.chat-msg__author:last').is(':visible')).toBeTruthy();
-                        expect(u.hasClass('chat-msg--followup', message_el)).toBeFalsy();
-                        done();
-                    });
-                });
+                _converse.chatboxes.onMessage(msg);
+                const view = _converse.chatboxviews.get(sender_jid);
+                await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(1);
+                expect(_.includes(view.el.querySelector('.chat-msg__author').textContent, '**Max Frankfurter')).toBeTruthy();
+                expect(view.el.querySelector('.chat-msg__text').textContent).toBe(' is tired');
+                message = '/me is as well';
+                await test_utils.sendMessage(view, message);
+                expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(2);
+                await test_utils.waitUntil(() => $(view.el).find('.chat-msg__author:last').text().trim() === '**Max Mustermann');
+                expect(sizzle('.chat-msg__text:last', view.el).pop().textContent).toBe(' is as well');
+                expect($(view.el).find('.chat-msg:last').hasClass('chat-msg--followup')).toBe(false);
+                // Check that /me messages after a normal message don't
+                // get the 'chat-msg--followup' class.
+                message = 'This a normal message';
+                await test_utils.sendMessage(view, message);
+                let message_el = view.el.querySelector('.message:last-child');
+                expect(u.hasClass('chat-msg--followup', message_el)).toBeFalsy();
+                message = '/me wrote a 3rd person message';
+                await test_utils.sendMessage(view, message);
+                message_el = view.el.querySelector('.message:last-child');
+                expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(3);
+                expect(sizzle('.chat-msg__text:last', view.el).pop().textContent).toBe(' wrote a 3rd person message');
+                expect(u.isVisible(sizzle('.chat-msg__author:last', view.el).pop())).toBeTruthy();
+                expect(u.hasClass('chat-msg--followup', message_el)).toBeFalsy();
+                done();
             }));
 
             it("is created when you click on a roster item", mock.initConverseWithPromises(
@@ -684,7 +672,7 @@
                     it("will be shown if received",
                         mock.initConverseWithPromises(
                             null, ['rosterGroupsFetched'], {},
-                            function (done, _converse) {
+                            async function (done, _converse) {
 
                         test_utils.createContacts(_converse, 'current');
                         test_utils.openControlBox();
@@ -705,28 +693,27 @@
                         expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
                         var view = _converse.chatboxviews.get(sender_jid);
                         expect(view).toBeDefined();
+                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
-                        test_utils.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1])
-                        .then(function () {
-                            var view = _converse.chatboxviews.get(sender_jid);
-                            // Check that the notification appears inside the chatbox in the DOM
-                            let events = view.el.querySelectorAll('.chat-state-notification');
-                            expect(events.length).toBe(1);
-                            expect(events[0].textContent).toEqual(mock.cur_names[1] + ' is typing');
+                        await test_utils.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1])
+                        // Check that the notification appears inside the chatbox in the DOM
+                        let events = view.el.querySelectorAll('.chat-state-notification');
+                        expect(events.length).toBe(1);
+                        expect(events[0].textContent).toEqual(mock.cur_names[1] + ' is typing');
 
-                            // Check that it doesn't appear twice
-                            msg = $msg({
-                                    from: sender_jid,
-                                    to: _converse.connection.jid,
-                                    type: 'chat',
-                                    id: (new Date()).getTime()
-                                }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                            _converse.chatboxes.onMessage(msg);
-                            events = view.el.querySelectorAll('.chat-state-notification');
-                            expect(events.length).toBe(1);
-                            expect(events[0].textContent).toEqual(mock.cur_names[1] + ' is typing');
-                            done();
-                        })
+                        // Check that it doesn't appear twice
+                        msg = $msg({
+                                from: sender_jid,
+                                to: _converse.connection.jid,
+                                type: 'chat',
+                                id: (new Date()).getTime()
+                            }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
+                        _converse.chatboxes.onMessage(msg);
+                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                        events = view.el.querySelectorAll('.chat-state-notification');
+                        expect(events.length).toBe(1);
+                        expect(events[0].textContent).toEqual(mock.cur_names[1] + ' is typing');
+                        done();
                     }));
 
                     it("can be a composing carbon message that this user sent from a different client",
@@ -841,38 +828,32 @@
                     }));
 
                     it("will be shown if received",
-                mock.initConverseWithPromises(
-                    null, ['rosterGroupsFetched'], {},
-                    function (done, _converse) {
+                            mock.initConverseWithPromises(
+                                null, ['rosterGroupsFetched'], {},
+                                async function (done, _converse) {
 
                         test_utils.createContacts(_converse, 'current');
                         test_utils.openControlBox();
-                        test_utils.waitUntil(function () {
-                                return $(_converse.rosterview.el).find('.roster-group').length;
-                            }, 300)
-                        .then(function () {
-                            // TODO: only show paused state if the previous state was composing
-                            // See XEP-0085 http://xmpp.org/extensions/xep-0085.html#definitions
-                            spyOn(_converse, 'emit');
-                            var sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
-                            // <paused> state
-                            var msg = $msg({
-                                    from: sender_jid,
-                                    to: _converse.connection.jid,
-                                    type: 'chat',
-                                    id: (new Date()).getTime()
-                                }).c('body').c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                            _converse.chatboxes.onMessage(msg);
-                            expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
-                            var view = _converse.chatboxviews.get(sender_jid);
-                            test_utils.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1])
-                            .then(function () {
-                                var view = _converse.chatboxviews.get(sender_jid);
-                                var event = view.el.querySelector('.chat-info.chat-state-notification');
-                                expect(event.textContent).toEqual(mock.cur_names[1] + ' has stopped typing');
-                                done();
-                            });
-                        });
+                        await test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
+                        // TODO: only show paused state if the previous state was composing
+                        // See XEP-0085 http://xmpp.org/extensions/xep-0085.html#definitions
+                        spyOn(_converse, 'emit');
+                        const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
+                        // <paused> state
+                        var msg = $msg({
+                                from: sender_jid,
+                                to: _converse.connection.jid,
+                                type: 'chat',
+                                id: (new Date()).getTime()
+                            }).c('body').c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
+                        _converse.chatboxes.onMessage(msg);
+                        expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
+                        const view = _converse.chatboxviews.get(sender_jid);
+                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                        await test_utils.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1])
+                        var event = view.el.querySelector('.chat-info.chat-state-notification');
+                        expect(event.textContent).toEqual(mock.cur_names[1] + ' has stopped typing');
+                        done();
                     }));
 
                     it("can be a paused carbon message that this user sent from a different client",
@@ -1242,14 +1223,11 @@
             it("is incremented from zero when chatbox was closed after viewing previously received messages and the window is not focused now",
                 mock.initConverseWithPromises(
                     null, ['rosterGroupsFetched'], {},
-                    function (done, _converse) {
+                    async function (done, _converse) {
 
                 test_utils.createContacts(_converse, 'current');
-
                 // initial state
                 expect(_converse.msg_counter).toBe(0);
-
-                let view;
                 const message = 'This message will always increment the message counter from zero',
                     sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost',
                     msgFactory = function () {
@@ -1267,29 +1245,28 @@
                 // leave converse-chat page
                 _converse.windowState = 'hidden';
                 _converse.chatboxes.onMessage(msgFactory());
-                return test_utils.waitUntil(() => _converse.api.chats.get().length)
-                .then(() => {
-                    expect(_converse.msg_counter).toBe(1);
+                await test_utils.waitUntil(() => _converse.api.chats.get().length)
+                let view = _converse.chatboxviews.get(sender_jid);
+                await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                expect(_converse.msg_counter).toBe(1);
 
-                    // come back to converse-chat page
-                    _converse.saveWindowState(null, 'focus');
-                    view = _converse.chatboxviews.get(sender_jid);
-                    expect(u.isVisible(view.el)).toBeTruthy();
-                    expect(_converse.msg_counter).toBe(0);
+                // come back to converse-chat page
+                _converse.saveWindowState(null, 'focus');
+                expect(u.isVisible(view.el)).toBeTruthy();
+                expect(_converse.msg_counter).toBe(0);
 
-                    // close chatbox and leave converse-chat page again
-                    view.close();
-                    _converse.windowState = 'hidden';
+                // close chatbox and leave converse-chat page again
+                view.close();
+                _converse.windowState = 'hidden';
 
-                    // check that msg_counter is incremented from zero again
-                    _converse.chatboxes.onMessage(msgFactory());
-                    return test_utils.waitUntil(() => _converse.api.chats.get().length)
-                }).then(() => {
-                    view = _converse.chatboxviews.get(sender_jid);
-                    expect(u.isVisible(view.el)).toBeTruthy();
-                    expect(_converse.msg_counter).toBe(1);
-                    done();
-                });
+                // check that msg_counter is incremented from zero again
+                _converse.chatboxes.onMessage(msgFactory());
+                await test_utils.waitUntil(() => _converse.api.chats.get().length)
+                view = _converse.chatboxviews.get(sender_jid);
+                await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                expect(u.isVisible(view.el)).toBeTruthy();
+                expect(_converse.msg_counter).toBe(1);
+                done();
             }));
         });
 

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 852 - 868
spec/chatroom.js


+ 250 - 268
spec/http-file-upload.js

@@ -268,224 +268,210 @@
 
                 describe("when clicked and a file chosen", function () {
 
-                    it("is uploaded and sent out", mock.initConverseWithAsync(function (done, _converse) {
-                        test_utils.waitUntilDiscoConfirmed(
+                    it("is uploaded and sent out", mock.initConverseWithAsync(
+                        async function (done, _converse) {
+
+                        await test_utils.waitUntilDiscoConfirmed(
                             _converse, _converse.domain,
                             [{'category': 'server', 'type':'IM'}],
-                            ['http://jabber.org/protocol/disco#items'], [], 'info').then(function () {
+                            ['http://jabber.org/protocol/disco#items'], [], 'info');
 
-                            var send_backup = XMLHttpRequest.prototype.send;
-                            var IQ_stanzas = _converse.connection.IQ_stanzas;
-                            let contact_jid;
+                        const send_backup = XMLHttpRequest.prototype.send;
+                        const IQ_stanzas = _converse.connection.IQ_stanzas;
+
+                        await test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items');
+                        await test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []);
+                        test_utils.createContacts(_converse, 'current');
+                        _converse.emit('rosterContactsFetched');
+                        const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
+                        await test_utils.openChatBoxFor(_converse, contact_jid);
+                        const view = _converse.chatboxviews.get(contact_jid);
+                        const file = {
+                            'type': 'image/jpeg',
+                            'size': '23456' ,
+                            'lastModifiedDate': "",
+                            'name': "my-juliet.jpg"
+                        };
+                        view.model.sendFiles([file]);
+                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+
+                        await test_utils.waitUntil(() => _.filter(IQ_stanzas, iq => iq.nodeTree.querySelector('iq[to="upload.montague.tld"] request')).length);
+                        var iq = IQ_stanzas.pop();
+                        expect(iq.toLocaleString()).toBe(
+                            `<iq from="dummy@localhost/resource" `+
+                                `id="${iq.nodeTree.getAttribute("id")}" `+
+                                `to="upload.montague.tld" `+
+                                `type="get" `+
+                                `xmlns="jabber:client">`+
+                            `<request `+
+                                `content-type="image/jpeg" `+
+                                `filename="my-juliet.jpg" `+
+                                `size="23456" `+
+                                `xmlns="urn:xmpp:http:upload:0"/>`+
+                            `</iq>`);
 
-                           test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items')
-                            .then(() => test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []))
+                        var base_url = document.URL.split(window.location.pathname)[0];
+                        var message = base_url+"/logo/conversejs-filled.svg";
+
+                        var stanza = Strophe.xmlHtmlNode(
+                            "<iq from='upload.montague.tld'"+
+                            "    id='"+iq.nodeTree.getAttribute('id')+"'"+
+                            "    to='dummy@localhost/resource'"+
+                            "    type='result'>"+
+                            "<slot xmlns='urn:xmpp:http:upload:0'>"+
+                            "    <put url='https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg'>"+
+                            "    <header name='Authorization'>Basic Base64String==</header>"+
+                            "    <header name='Cookie'>foo=bar; user=romeo</header>"+
+                            "    </put>"+
+                            "    <get url='"+message+"' />"+
+                            "</slot>"+
+                            "</iq>").firstElementChild;
+
+                        spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () {
+                            const message = view.model.messages.at(0);
+                            expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
+                            message.set('progress', 0.5);
+                            test_utils.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '0.5')
                             .then(() => {
-                                test_utils.createContacts(_converse, 'current');
-                                _converse.emit('rosterContactsFetched');
-                                contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
-                                return test_utils.openChatBoxFor(_converse, contact_jid);
+                                message.set('progress', 1);
+                                test_utils.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '1')
                             }).then(() => {
-                                var view = _converse.chatboxviews.get(contact_jid);
-                                var file = {
-                                    'type': 'image/jpeg',
-                                    'size': '23456' ,
-                                    'lastModifiedDate': "",
-                                    'name': "my-juliet.jpg"
-                                };
-                                view.model.sendFiles([file]);
-                                return test_utils.waitUntil(function () {
-                                    return _.filter(IQ_stanzas, function (iq) {
-                                        return iq.nodeTree.querySelector('iq[to="upload.montague.tld"] request');
-                                    }).length > 0;
-                                }).then(function () {
-                                    var iq = IQ_stanzas.pop();
-                                    expect(iq.toLocaleString()).toBe(
-                                        `<iq from="dummy@localhost/resource" `+
-                                            `id="${iq.nodeTree.getAttribute("id")}" `+
-                                            `to="upload.montague.tld" `+
-                                            `type="get" `+
-                                            `xmlns="jabber:client">`+
-                                        `<request `+
-                                            `content-type="image/jpeg" `+
-                                            `filename="my-juliet.jpg" `+
-                                            `size="23456" `+
-                                            `xmlns="urn:xmpp:http:upload:0"/>`+
-                                        `</iq>`);
-
-                                    var base_url = document.URL.split(window.location.pathname)[0];
-                                    var message = base_url+"/logo/conversejs-filled.svg";
-
-                                    var stanza = Strophe.xmlHtmlNode(
-                                        "<iq from='upload.montague.tld'"+
-                                        "    id='"+iq.nodeTree.getAttribute('id')+"'"+
-                                        "    to='dummy@localhost/resource'"+
-                                        "    type='result'>"+
-                                        "<slot xmlns='urn:xmpp:http:upload:0'>"+
-                                        "    <put url='https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg'>"+
-                                        "    <header name='Authorization'>Basic Base64String==</header>"+
-                                        "    <header name='Cookie'>foo=bar; user=romeo</header>"+
-                                        "    </put>"+
-                                        "    <get url='"+message+"' />"+
-                                        "</slot>"+
-                                        "</iq>").firstElementChild;
-
-                                    spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () {
-                                        const message = view.model.messages.at(0);
-                                        expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
-                                        message.set('progress', 0.5);
-                                        expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0.5');
-                                        message.set('progress', 1);
-                                        expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('1');
-                                        message.save({
-                                            'upload': _converse.SUCCESS,
-                                            'oob_url': message.get('get'),
-                                            'message': message.get('get')
-                                        });
-                                    });
-                                    var sent_stanza;
-                                    spyOn(_converse.connection, 'send').and.callFake(function (stanza) {
-                                        sent_stanza = stanza;
-                                    });
-                                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
-
-                                    return test_utils.waitUntil(function () {
-                                        return sent_stanza;
-                                    }, 1000).then(function () {
-                                        expect(sent_stanza.toLocaleString()).toBe(
-                                            `<message from="dummy@localhost/resource" `+
-                                                `id="${sent_stanza.nodeTree.getAttribute("id")}" `+
-                                                `to="irini.vlastuin@localhost" `+
-                                                `type="chat" `+
-                                                `xmlns="jabber:client">`+
-                                                    `<body>${message}</body>`+
-                                                    `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
-                                                    `<x xmlns="jabber:x:oob">`+
-                                                        `<url>${message}</url>`+
-                                                    `</x>`+
-                                            `</message>`);
-                                        return test_utils.waitUntil(() => view.el.querySelector('.chat-image'), 1000);
-                                    }).then(function () {
-                                        // Check that the image renders
-                                        expect(view.el.querySelector('.chat-msg .chat-msg__media').innerHTML.trim()).toEqual(
-                                            `<!-- src/templates/image.html -->\n`+
-                                            `<a href="${window.location.origin}/logo/conversejs-filled.svg" target="_blank" rel="noopener">`+
-                                                `<img class="chat-image img-thumbnail" src="${window.location.origin}/logo/conversejs-filled.svg">`+
-                                            `</a>`);
-                                        XMLHttpRequest.prototype.send = send_backup;
-                                        done();
-                                    });
+                                message.save({
+                                    'upload': _converse.SUCCESS,
+                                    'oob_url': message.get('get'),
+                                    'message': message.get('get')
                                 });
+                                return new Promise((resolve, reject) => view.model.messages.once('rendered', resolve));
                             });
                         });
+                        let sent_stanza;
+                        spyOn(_converse.connection, 'send').and.callFake(stanza => (sent_stanza = stanza));
+                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                        await test_utils.waitUntil(() => sent_stanza, 1000);
+                        expect(sent_stanza.toLocaleString()).toBe(
+                            `<message from="dummy@localhost/resource" `+
+                                `id="${sent_stanza.nodeTree.getAttribute("id")}" `+
+                                `to="irini.vlastuin@localhost" `+
+                                `type="chat" `+
+                                `xmlns="jabber:client">`+
+                                    `<body>${message}</body>`+
+                                    `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
+                                    `<x xmlns="jabber:x:oob">`+
+                                        `<url>${message}</url>`+
+                                    `</x>`+
+                            `</message>`);
+                        await test_utils.waitUntil(() => view.el.querySelector('.chat-image'), 1000);
+                        // Check that the image renders
+                        expect(view.el.querySelector('.chat-msg .chat-msg__media').innerHTML.trim()).toEqual(
+                            `<!-- src/templates/image.html -->\n`+
+                            `<a href="${window.location.origin}/logo/conversejs-filled.svg" target="_blank" rel="noopener">`+
+                                `<img class="chat-image img-thumbnail" src="${window.location.origin}/logo/conversejs-filled.svg">`+
+                            `</a>`);
+                        XMLHttpRequest.prototype.send = send_backup;
+                        done();
                     }));
 
-                    it("is uploaded and sent out from a groupchat", mock.initConverseWithAsync(function (done, _converse) {
-                        test_utils.waitUntilDiscoConfirmed(
+                    it("is uploaded and sent out from a groupchat", mock.initConverseWithAsync(
+                        async function (done, _converse) {
+
+                        await test_utils.waitUntilDiscoConfirmed(
                             _converse, _converse.domain,
                             [{'category': 'server', 'type':'IM'}],
-                            ['http://jabber.org/protocol/disco#items'], [], 'info').then(function () {
+                            ['http://jabber.org/protocol/disco#items'], [], 'info');
+
+                        const send_backup = XMLHttpRequest.prototype.send;
+                        const IQ_stanzas = _converse.connection.IQ_stanzas;
 
-                            var send_backup = XMLHttpRequest.prototype.send;
-                            var IQ_stanzas = _converse.connection.IQ_stanzas;
-
-                            test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items').then(function () {
-                                test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []).then(function () {
-                                    test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy').then(function () {
-                                        var view = _converse.chatboxviews.get('lounge@localhost');
-                                        var file = {
-                                            'type': 'image/jpeg',
-                                            'size': '23456' ,
-                                            'lastModifiedDate': "",
-                                            'name': "my-juliet.jpg"
-                                        };
-                                        view.model.sendFiles([file]);
-
-                                        return test_utils.waitUntil(function () {
-                                            return _.filter(IQ_stanzas, function (iq) {
-                                                return iq.nodeTree.querySelector('iq[to="upload.montague.tld"] request');
-                                            }).length > 0;
-                                        }).then(function () {
-                                            var iq = IQ_stanzas.pop();
-                                            expect(iq.toLocaleString()).toBe(
-                                                `<iq from="dummy@localhost/resource" `+
-                                                    `id="${iq.nodeTree.getAttribute("id")}" `+
-                                                    `to="upload.montague.tld" `+
-                                                    `type="get" `+
-                                                    `xmlns="jabber:client">`+
-                                                `<request `+
-                                                    `content-type="image/jpeg" `+
-                                                    `filename="my-juliet.jpg" `+
-                                                    `size="23456" `+
-                                                    `xmlns="urn:xmpp:http:upload:0"/>`+
-                                                `</iq>`);
-
-                                            var base_url = document.URL.split(window.location.pathname)[0];
-                                            var message = base_url+"/logo/conversejs-filled.svg";
-
-                                            var stanza = Strophe.xmlHtmlNode(
-                                                "<iq from='upload.montague.tld'"+
-                                                "    id='"+iq.nodeTree.getAttribute('id')+"'"+
-                                                "    to='dummy@localhost/resource'"+
-                                                "    type='result'>"+
-                                                "<slot xmlns='urn:xmpp:http:upload:0'>"+
-                                                "    <put url='https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg'>"+
-                                                "    <header name='Authorization'>Basic Base64String==</header>"+
-                                                "    <header name='Cookie'>foo=bar; user=romeo</header>"+
-                                                "    </put>"+
-                                                "    <get url='"+message+"' />"+
-                                                "</slot>"+
-                                                "</iq>").firstElementChild;
-
-                                            spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () {
-                                                const message = view.model.messages.at(0);
-                                                expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
-                                                message.set('progress', 0.5);
-                                                expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0.5');
-                                                message.set('progress', 1);
-                                                expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('1');
-                                                message.save({
-                                                    'upload': _converse.SUCCESS,
-                                                    'oob_url': message.get('get'),
-                                                    'message': message.get('get')
-                                                });
-                                            });
-                                            var sent_stanza;
-                                            spyOn(_converse.connection, 'send').and.callFake(function (stanza) {
-                                                sent_stanza = stanza;
-                                            });
-                                            _converse.connection._dataRecv(test_utils.createRequest(stanza));
-
-                                            return test_utils.waitUntil(() => sent_stanza, 1000).then(function () {
-                                                expect(sent_stanza.toLocaleString()).toBe(
-                                                    `<message `+
-                                                        `from="dummy@localhost/resource" `+
-                                                        `id="${sent_stanza.nodeTree.getAttribute("id")}" `+
-                                                        `to="lounge@localhost" `+
-                                                        `type="groupchat" `+
-                                                        `xmlns="jabber:client">`+
-                                                            `<body>${message}</body>`+
-                                                            `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
-                                                            `<x xmlns="jabber:x:oob">`+
-                                                                `<url>${message}</url>`+
-                                                            `</x>`+
-                                                    `</message>`);
-                                                return test_utils.waitUntil(() => view.el.querySelector('.chat-image'), 1000);
-                                            }).then(function () {
-                                                // Check that the image renders
-                                                expect(view.el.querySelector('.chat-msg .chat-msg__media').innerHTML.trim()).toEqual(
-                                                    `<!-- src/templates/image.html -->\n`+
-                                                    `<a href="${window.location.origin}/logo/conversejs-filled.svg" target="_blank" rel="noopener">`+
-                                                        `<img class="chat-image img-thumbnail" src="${window.location.origin}/logo/conversejs-filled.svg">`+
-                                                    `</a>`);
-                                                XMLHttpRequest.prototype.send = send_backup;
-                                                done();
-                                            });
-                                        });
-                                    });
+                        await test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items');
+                        await test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []);
+                        await test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy');
+                        const view = _converse.chatboxviews.get('lounge@localhost');
+                        const file = {
+                            'type': 'image/jpeg',
+                            'size': '23456' ,
+                            'lastModifiedDate': "",
+                            'name': "my-juliet.jpg"
+                        };
+                        view.model.sendFiles([file]);
+                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+
+                        await test_utils.waitUntil(() => _.filter(IQ_stanzas, iq => iq.nodeTree.querySelector('iq[to="upload.montague.tld"] request')).length);
+                        var iq = IQ_stanzas.pop();
+                        expect(iq.toLocaleString()).toBe(
+                            `<iq from="dummy@localhost/resource" `+
+                                `id="${iq.nodeTree.getAttribute("id")}" `+
+                                `to="upload.montague.tld" `+
+                                `type="get" `+
+                                `xmlns="jabber:client">`+
+                            `<request `+
+                                `content-type="image/jpeg" `+
+                                `filename="my-juliet.jpg" `+
+                                `size="23456" `+
+                                `xmlns="urn:xmpp:http:upload:0"/>`+
+                            `</iq>`);
+
+                        var base_url = document.URL.split(window.location.pathname)[0];
+                        var message = base_url+"/logo/conversejs-filled.svg";
+
+                        var stanza = Strophe.xmlHtmlNode(
+                            "<iq from='upload.montague.tld'"+
+                            "    id='"+iq.nodeTree.getAttribute('id')+"'"+
+                            "    to='dummy@localhost/resource'"+
+                            "    type='result'>"+
+                            "<slot xmlns='urn:xmpp:http:upload:0'>"+
+                            "    <put url='https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg'>"+
+                            "    <header name='Authorization'>Basic Base64String==</header>"+
+                            "    <header name='Cookie'>foo=bar; user=romeo</header>"+
+                            "    </put>"+
+                            "    <get url='"+message+"' />"+
+                            "</slot>"+
+                            "</iq>").firstElementChild;
+
+                        spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () {
+                            const message = view.model.messages.at(0);
+                            expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
+                            message.set('progress', 0.5);
+                            test_utils.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '0.5')
+                            .then(() => {
+                                message.set('progress', 1);
+                                test_utils.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '1')
+                            }).then(() => {
+                                message.save({
+                                    'upload': _converse.SUCCESS,
+                                    'oob_url': message.get('get'),
+                                    'message': message.get('get')
                                 });
+                                return new Promise((resolve, reject) => view.model.messages.once('rendered', resolve));
                             });
                         });
+                        let sent_stanza;
+                        spyOn(_converse.connection, 'send').and.callFake(stanza => (sent_stanza = stanza));
+                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                        await test_utils.waitUntil(() => sent_stanza, 1000);
+                        expect(sent_stanza.toLocaleString()).toBe(
+                            `<message `+
+                                `from="dummy@localhost/resource" `+
+                                `id="${sent_stanza.nodeTree.getAttribute("id")}" `+
+                                `to="lounge@localhost" `+
+                                `type="groupchat" `+
+                                `xmlns="jabber:client">`+
+                                    `<body>${message}</body>`+
+                                    `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
+                                    `<x xmlns="jabber:x:oob">`+
+                                        `<url>${message}</url>`+
+                                    `</x>`+
+                            `</message>`);
+                        await test_utils.waitUntil(() => view.el.querySelector('.chat-image'), 1000);
+                        // Check that the image renders
+                        expect(view.el.querySelector('.chat-msg .chat-msg__media').innerHTML.trim()).toEqual(
+                            `<!-- src/templates/image.html -->\n`+
+                            `<a href="${window.location.origin}/logo/conversejs-filled.svg" target="_blank" rel="noopener">`+
+                                `<img class="chat-image img-thumbnail" src="${window.location.origin}/logo/conversejs-filled.svg">`+
+                            `</a>`);
+                        XMLHttpRequest.prototype.send = send_backup;
+                        done();
                     }));
 
                     it("shows an error message if the file is too large", mock.initConverseWithAsync(function (done, _converse) {
@@ -617,82 +603,78 @@
             describe("While a file is being uploaded", function () {
 
                 it("shows a progress bar", mock.initConverseWithPromises(
-                            null, ['rosterGroupsFetched', 'chatBoxesFetched'], {}, function (done, _converse) {
+                    null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                    async function (done, _converse) {
 
-                    test_utils.waitUntilDiscoConfirmed(
+                    await test_utils.waitUntilDiscoConfirmed(
                         _converse, _converse.domain,
                         [{'category': 'server', 'type':'IM'}],
-                        ['http://jabber.org/protocol/disco#items'], [], 'info').then(function () {
+                        ['http://jabber.org/protocol/disco#items'], [], 'info');
 
-                        var send_backup = XMLHttpRequest.prototype.send;
-                        var IQ_stanzas = _converse.connection.IQ_stanzas;
-                        let view, contact_jid;
+                    const send_backup = XMLHttpRequest.prototype.send;
+                    const IQ_stanzas = _converse.connection.IQ_stanzas;
 
-                        test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items')
-                        .then(() => test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []))
+                    await test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items');
+                    await test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []);
+                    test_utils.createContacts(_converse, 'current');
+                    _converse.emit('rosterContactsFetched');
+                    const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
+                    await test_utils.openChatBoxFor(_converse, contact_jid);
+                    const view = _converse.chatboxviews.get(contact_jid);
+                    const file = {
+                        'type': 'image/jpeg',
+                        'size': '23456' ,
+                        'lastModifiedDate': "",
+                        'name': "my-juliet.jpg"
+                    };
+                    view.model.sendFiles([file]);
+                    await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                    await test_utils.waitUntil(() => _.filter(IQ_stanzas, (iq) => iq.nodeTree.querySelector('iq[to="upload.montague.tld"] request')).length)
+                    const iq = IQ_stanzas.pop();
+                    expect(iq.toLocaleString()).toBe(
+                        `<iq from="dummy@localhost/resource" `+
+                            `id="${iq.nodeTree.getAttribute("id")}" `+
+                            `to="upload.montague.tld" `+
+                            `type="get" `+
+                            `xmlns="jabber:client">`+
+                        `<request `+
+                            `content-type="image/jpeg" `+
+                            `filename="my-juliet.jpg" `+
+                            `size="23456" `+
+                            `xmlns="urn:xmpp:http:upload:0"/>`+
+                        `</iq>`);
+
+                    const base_url = document.URL.split(window.location.pathname)[0];
+                    const message = base_url+"/logo/conversejs-filled.svg";
+                    const stanza = Strophe.xmlHtmlNode(
+                        "<iq from='upload.montague.tld'"+
+                        "    id='"+iq.nodeTree.getAttribute('id')+"'"+
+                        "    to='dummy@localhost/resource'"+
+                        "    type='result'>"+
+                        "<slot xmlns='urn:xmpp:http:upload:0'>"+
+                        "    <put url='https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg'>"+
+                        "    <header name='Authorization'>Basic Base64String==</header>"+
+                        "    <header name='Cookie'>foo=bar; user=romeo</header>"+
+                        "    </put>"+
+                        "    <get url='"+message+"' />"+
+                        "</slot>"+
+                        "</iq>").firstElementChild;
+                    spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () {
+                        const message = view.model.messages.at(0);
+                        expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
+                        message.set('progress', 0.5);
+                        test_utils.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '0.5')
                         .then(() => {
-                            test_utils.createContacts(_converse, 'current');
-                            _converse.emit('rosterContactsFetched');
-
-                            contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
-                            return test_utils.openChatBoxFor(_converse, contact_jid);
+                            message.set('progress', 1);
+                            test_utils.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '1')
                         }).then(() => {
-                            view = _converse.chatboxviews.get(contact_jid);
-                            const file = {
-                                'type': 'image/jpeg',
-                                'size': '23456' ,
-                                'lastModifiedDate': "",
-                                'name': "my-juliet.jpg"
-                            };
-                            view.model.sendFiles([file]);
-                            return test_utils.waitUntil(() => _.filter(IQ_stanzas, (iq) => iq.nodeTree.querySelector('iq[to="upload.montague.tld"] request')).length)
-                        }).then(function () {
-                            const iq = IQ_stanzas.pop();
-                            expect(iq.toLocaleString()).toBe(
-                                `<iq from="dummy@localhost/resource" `+
-                                    `id="${iq.nodeTree.getAttribute("id")}" `+
-                                    `to="upload.montague.tld" `+
-                                    `type="get" `+
-                                    `xmlns="jabber:client">`+
-                                `<request `+
-                                    `content-type="image/jpeg" `+
-                                    `filename="my-juliet.jpg" `+
-                                    `size="23456" `+
-                                    `xmlns="urn:xmpp:http:upload:0"/>`+
-                                `</iq>`);
-
-                            const base_url = document.URL.split(window.location.pathname)[0];
-                            const message = base_url+"/logo/conversejs-filled.svg";
-                            const stanza = Strophe.xmlHtmlNode(
-                                "<iq from='upload.montague.tld'"+
-                                "    id='"+iq.nodeTree.getAttribute('id')+"'"+
-                                "    to='dummy@localhost/resource'"+
-                                "    type='result'>"+
-                                "<slot xmlns='urn:xmpp:http:upload:0'>"+
-                                "    <put url='https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg'>"+
-                                "    <header name='Authorization'>Basic Base64String==</header>"+
-                                "    <header name='Cookie'>foo=bar; user=romeo</header>"+
-                                "    </put>"+
-                                "    <get url='"+message+"' />"+
-                                "</slot>"+
-                                "</iq>").firstElementChild;
-                            spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () {
-                                const message = view.model.messages.at(0);
-                                expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
-                                message.set('progress', 0.5);
-                                expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0.5');
-                                message.set('progress', 1);
-                                expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('1');
-                                expect(view.el.querySelector('.chat-content .chat-msg__text').textContent).toBe('Uploading file: my-juliet.jpg, 22.91 KB');
-                                done();
-                            });
-                            var sent_stanza;
-                            spyOn(_converse.connection, 'send').and.callFake(function (stanza) {
-                                sent_stanza = stanza;
-                            });
-                            _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                            expect(view.el.querySelector('.chat-content .chat-msg__text').textContent).toBe('Uploading file: my-juliet.jpg, 22.91 KB');
+                            done();
                         });
                     });
+                    let sent_stanza;
+                    spyOn(_converse.connection, 'send').and.callFake(stanza => (sent_stanza = stanza));
+                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
                 }));
             });
         });

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 451 - 449
spec/messages.js


+ 91 - 98
spec/roomslist.js

@@ -95,113 +95,106 @@
             { whitelisted_plugins: ['converse-roomslist'],
               allow_bookmarks: false // Makes testing easier, otherwise we
                                      // have to mock stanza traffic.
-            }, function (done, _converse) {
+            }, async function (done, _converse) {
 
-            let view;
             const IQ_stanzas = _converse.connection.IQ_stanzas;
             const room_jid = 'coven@chat.shakespeare.lit';
             test_utils.openControlBox();
-            _converse.api.rooms.open(room_jid, {'nick': 'some1'})
-            .then(() => {
-                return test_utils.waitUntil(() => _.get(_.filter(
-                    IQ_stanzas,
-                    iq => iq.nodeTree.querySelector(
-                        `iq[to="${room_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
-                    )).pop(), 'nodeTree'));
-            }).then(last_stanza => {
-                view = _converse.chatboxviews.get(room_jid);
-                const IQ_id = last_stanza.getAttribute('id');
-                const features_stanza = $iq({
-                        'from': 'coven@chat.shakespeare.lit',
-                        'id': IQ_id,
-                        'to': 'dummy@localhost/desktop',
-                        'type': 'result'
-                    })
-                    .c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'})
-                        .c('identity', {
-                            'category': 'conference',
-                            'name': 'A Dark Cave',
-                            'type': 'text'
-                        }).up()
-                        .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up()
-                        .c('feature', {'var': 'muc_passwordprotected'}).up()
-                        .c('feature', {'var': 'muc_hidden'}).up()
-                        .c('feature', {'var': 'muc_temporary'}).up()
-                        .c('feature', {'var': 'muc_open'}).up()
-                        .c('feature', {'var': 'muc_unmoderated'}).up()
-                        .c('feature', {'var': 'muc_nonanonymous'}).up()
-                        .c('feature', {'var': 'urn:xmpp:mam:0'}).up()
-                        .c('x', { 'xmlns':'jabber:x:data', 'type':'result'})
-                            .c('field', {'var':'FORM_TYPE', 'type':'hidden'})
-                                .c('value').t('http://jabber.org/protocol/muc#roominfo').up().up()
-                            .c('field', {'type':'text-single', 'var':'muc#roominfo_description', 'label':'Description'})
-                                .c('value').t('This is the description').up().up()
-                            .c('field', {'type':'text-single', 'var':'muc#roominfo_occupants', 'label':'Number of occupants'})
-                                .c('value').t(0);
-                _converse.connection._dataRecv(test_utils.createRequest(features_stanza));
-                return test_utils.waitUntil(() => view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING)
-            }).then(function () {
-                var presence = $pres({
-                        to: _converse.connection.jid,
-                        from: 'coven@chat.shakespeare.lit/some1',
-                        id: 'DC352437-C019-40EC-B590-AF29E879AF97'
-                }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
-                    .c('item').attrs({
-                        affiliation: 'member',
-                        jid: _converse.bare_jid,
-                        role: 'participant'
+            await _converse.api.rooms.open(room_jid, {'nick': 'some1'});
+            const last_stanza = await test_utils.waitUntil(() => _.get(_.filter(
+                IQ_stanzas,
+                iq => iq.nodeTree.querySelector(
+                    `iq[to="${room_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
+                )).pop(), 'nodeTree'));
+            const view = _converse.chatboxviews.get(room_jid);
+            const IQ_id = last_stanza.getAttribute('id');
+            const features_stanza = $iq({
+                    'from': 'coven@chat.shakespeare.lit',
+                    'id': IQ_id,
+                    'to': 'dummy@localhost/desktop',
+                    'type': 'result'
+                })
+                .c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'})
+                    .c('identity', {
+                        'category': 'conference',
+                        'name': 'A Dark Cave',
+                        'type': 'text'
                     }).up()
-                    .c('status').attrs({code:'110'});
-                _converse.connection._dataRecv(test_utils.createRequest(presence));
+                    .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up()
+                    .c('feature', {'var': 'muc_passwordprotected'}).up()
+                    .c('feature', {'var': 'muc_hidden'}).up()
+                    .c('feature', {'var': 'muc_temporary'}).up()
+                    .c('feature', {'var': 'muc_open'}).up()
+                    .c('feature', {'var': 'muc_unmoderated'}).up()
+                    .c('feature', {'var': 'muc_nonanonymous'}).up()
+                    .c('feature', {'var': 'urn:xmpp:mam:0'}).up()
+                    .c('x', { 'xmlns':'jabber:x:data', 'type':'result'})
+                        .c('field', {'var':'FORM_TYPE', 'type':'hidden'})
+                            .c('value').t('http://jabber.org/protocol/muc#roominfo').up().up()
+                        .c('field', {'type':'text-single', 'var':'muc#roominfo_description', 'label':'Description'})
+                            .c('value').t('This is the description').up().up()
+                        .c('field', {'type':'text-single', 'var':'muc#roominfo_occupants', 'label':'Number of occupants'})
+                            .c('value').t(0);
+            _converse.connection._dataRecv(test_utils.createRequest(features_stanza));
+            await test_utils.waitUntil(() => view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING)
+            let presence = $pres({
+                    to: _converse.connection.jid,
+                    from: 'coven@chat.shakespeare.lit/some1',
+                    id: 'DC352437-C019-40EC-B590-AF29E879AF97'
+            }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
+                .c('item').attrs({
+                    affiliation: 'member',
+                    jid: _converse.bare_jid,
+                    role: 'participant'
+                }).up()
+                .c('status').attrs({code:'110'});
+            _converse.connection._dataRecv(test_utils.createRequest(presence));
 
-                const room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
-                expect(room_els.length).toBe(1);
-                const info_el = _converse.rooms_list_view.el.querySelector(".room-info");
-                info_el.click();
+            const room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
+            expect(room_els.length).toBe(1);
+            const info_el = _converse.rooms_list_view.el.querySelector(".room-info");
+            info_el.click();
 
-                const modal = view.model.room_details_modal;
-                return test_utils.waitUntil(() => u.isVisible(modal.el), 2000);
-            }).then(() => {
-                const modal = view.model.room_details_modal;
-                let els = modal.el.querySelectorAll('p.room-info');
-                expect(els[0].textContent).toBe("Name: A Dark Cave")
-                expect(els[1].textContent).toBe("Groupchat address (JID): coven@chat.shakespeare.lit")
-                expect(els[2].textContent).toBe("Description: This is the description")
-                expect(els[3].textContent).toBe("Online users: 1")
-                const features_list = modal.el.querySelector('.features-list');
-                expect(features_list.textContent.replace(/(\n|\s{2,})/g, '')).toBe(
-                    'Password protected - This groupchat requires a password before entry'+
-                    'Hidden - This groupchat is not publicly searchable'+
-                    'Open - Anyone can join this groupchat'+
-                    'Temporary - This groupchat will disappear once the last person leaves'+
-                    'Not anonymous - All other groupchat participants can see your XMPP username'+
-                    'Not moderated - Participants entering this groupchat can write right away'
-                );
-                const presence = $pres({
-                        to: 'dummy@localhost/_converse.js-29092160',
-                        from: 'coven@chat.shakespeare.lit/newguy'
-                    })
-                    .c('x', {xmlns: Strophe.NS.MUC_USER})
-                    .c('item', {
-                        'affiliation': 'none',
-                        'jid': 'newguy@localhost/_converse.js-290929789',
-                        'role': 'participant'
-                    });
-                _converse.connection._dataRecv(test_utils.createRequest(presence));
+            const  modal = view.model.room_details_modal;
+            await test_utils.waitUntil(() => u.isVisible(modal.el), 2000);
+            let els = modal.el.querySelectorAll('p.room-info');
+            expect(els[0].textContent).toBe("Name: A Dark Cave")
+            expect(els[1].textContent).toBe("Groupchat address (JID): coven@chat.shakespeare.lit")
+            expect(els[2].textContent).toBe("Description: This is the description")
+            expect(els[3].textContent).toBe("Online users: 1")
+            const features_list = modal.el.querySelector('.features-list');
+            expect(features_list.textContent.replace(/(\n|\s{2,})/g, '')).toBe(
+                'Password protected - This groupchat requires a password before entry'+
+                'Hidden - This groupchat is not publicly searchable'+
+                'Open - Anyone can join this groupchat'+
+                'Temporary - This groupchat will disappear once the last person leaves'+
+                'Not anonymous - All other groupchat participants can see your XMPP username'+
+                'Not moderated - Participants entering this groupchat can write right away'
+            );
+            presence = $pres({
+                    to: 'dummy@localhost/_converse.js-29092160',
+                    from: 'coven@chat.shakespeare.lit/newguy'
+                })
+                .c('x', {xmlns: Strophe.NS.MUC_USER})
+                .c('item', {
+                    'affiliation': 'none',
+                    'jid': 'newguy@localhost/_converse.js-290929789',
+                    'role': 'participant'
+                });
+            _converse.connection._dataRecv(test_utils.createRequest(presence));
 
-                els = modal.el.querySelectorAll('p.room-info');
-                expect(els[3].textContent).toBe("Online users: 2")
+            els = modal.el.querySelectorAll('p.room-info');
+            expect(els[3].textContent).toBe("Online users: 2")
 
-                view.model.set({'subject': {'author': 'someone', 'text': 'Hatching dark plots'}});
-                els = modal.el.querySelectorAll('p.room-info');
-                expect(els[0].textContent).toBe("Name: A Dark Cave")
-                expect(els[1].textContent).toBe("Groupchat address (JID): coven@chat.shakespeare.lit")
-                expect(els[2].textContent).toBe("Description: This is the description")
-                expect(els[3].textContent).toBe("Topic: Hatching dark plots")
-                expect(els[4].textContent).toBe("Topic author: someone")
-                expect(els[5].textContent).toBe("Online users: 2")
-                done();
-            }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+            view.model.set({'subject': {'author': 'someone', 'text': 'Hatching dark plots'}});
+            els = modal.el.querySelectorAll('p.room-info');
+            expect(els[0].textContent).toBe("Name: A Dark Cave")
+            expect(els[1].textContent).toBe("Groupchat address (JID): coven@chat.shakespeare.lit")
+            expect(els[2].textContent).toBe("Description: This is the description")
+            expect(els[3].textContent).toBe("Topic: Hatching dark plots")
+            expect(els[4].textContent).toBe("Topic author: someone")
+            expect(els[5].textContent).toBe("Online users: 2")
+            done();
         }));
 
         it("can be closed", mock.initConverseWithPromises(

+ 147 - 160
spec/spoilers.js

@@ -1,30 +1,30 @@
 (function (root, factory) {
     define(["jasmine", "mock", "test-utils"], factory);
 } (this, function (jasmine, mock, test_utils) {
-    var _ = converse.env._;
-    var Strophe = converse.env.Strophe;
-    var $msg = converse.env.$msg;
-    var $pres = converse.env.$pres;
-    var u = converse.env.utils;
+    const _ = converse.env._;
+    const Strophe = converse.env.Strophe;
+    const $msg = converse.env.$msg;
+    const $pres = converse.env.$pres;
+    const u = converse.env.utils;
 
-    return describe("A spoiler message", function () {
+    describe("A spoiler message", function () {
 
         it("can be received with a hint",
             mock.initConverseWithPromises(
                 null, ['rosterGroupsFetched'], {},
-                function (done, _converse) {
+                async function (done, _converse) {
 
             test_utils.createContacts(_converse, 'current');
-            var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+            const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
 
             /* <message to='romeo@montague.net/orchard' from='juliet@capulet.net/balcony' id='spoiler2'>
              *      <body>And at the end of the story, both of them die! It is so tragic!</body>
              *      <spoiler xmlns='urn:xmpp:spoiler:0'>Love story end</spoiler>
              *  </message>
              */
-            var spoiler_hint = "Love story end"
-            var spoiler = "And at the end of the story, both of them die! It is so tragic!";
-            var msg = $msg({
+            const spoiler_hint = "Love story end"
+            const spoiler = "And at the end of the story, both of them die! It is so tragic!";
+            const msg = $msg({
                     'xmlns': 'jabber:client',
                     'to': _converse.bare_jid,
                     'from': sender_jid,
@@ -36,36 +36,30 @@
                 .tree();
             _converse.chatboxes.onMessage(msg);
 
-            var view = _converse.chatboxviews.get(sender_jid);
-
-            return test_utils.waitUntil(() => view.model.vcard.get('fullname') === 'Max Frankfurter')
-            .then(function () {
-                expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Max Frankfurter');
-
-                var message_content = view.el.querySelector('.chat-msg__text');
-                expect(message_content.textContent).toBe(spoiler);
-
-                var spoiler_hint_el = view.el.querySelector('.spoiler-hint');
-                expect(spoiler_hint_el.textContent).toBe(spoiler_hint);
-                done();
-            });
+            const view = _converse.chatboxviews.get(sender_jid);
+            await test_utils.waitUntil(() => view.model.vcard.get('fullname') === 'Max Frankfurter')
+            expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Max Frankfurter');
+            const message_content = view.el.querySelector('.chat-msg__text');
+            expect(message_content.textContent).toBe(spoiler);
+            const spoiler_hint_el = view.el.querySelector('.spoiler-hint');
+            expect(spoiler_hint_el.textContent).toBe(spoiler_hint);
+            done();
         }));
 
         it("can be received without a hint",
             mock.initConverseWithPromises(
                 null, ['rosterGroupsFetched'], {},
-                function (done, _converse) {
+                async function (done, _converse) {
 
             test_utils.createContacts(_converse, 'current');
-            var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
-
+            const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
             /* <message to='romeo@montague.net/orchard' from='juliet@capulet.net/balcony' id='spoiler2'>
              *      <body>And at the end of the story, both of them die! It is so tragic!</body>
              *      <spoiler xmlns='urn:xmpp:spoiler:0'>Love story end</spoiler>
              *  </message>
              */
-            var spoiler = "And at the end of the story, both of them die! It is so tragic!";
-            var msg = $msg({
+            const spoiler = "And at the end of the story, both of them die! It is so tragic!";
+            const msg = $msg({
                     'xmlns': 'jabber:client',
                     'to': _converse.bare_jid,
                     'from': sender_jid,
@@ -75,25 +69,20 @@
                       'xmlns': 'urn:xmpp:spoiler:0',
                     }).tree();
             _converse.chatboxes.onMessage(msg);
-
-            var view = _converse.chatboxviews.get(sender_jid);
-            return test_utils.waitUntil(() => view.model.vcard.get('fullname') === 'Max Frankfurter')
-            .then(function () {
-                expect(_.includes(view.el.querySelector('.chat-msg__author').textContent, 'Max Frankfurter')).toBeTruthy();
-
-                var message_content = view.el.querySelector('.chat-msg__text');
-                expect(message_content.textContent).toBe(spoiler);
-
-                var spoiler_hint_el = view.el.querySelector('.spoiler-hint');
-                expect(spoiler_hint_el.textContent).toBe('');
-                done();
-            }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+            const view = _converse.chatboxviews.get(sender_jid);
+            await test_utils.waitUntil(() => view.model.vcard.get('fullname') === 'Max Frankfurter')
+            expect(_.includes(view.el.querySelector('.chat-msg__author').textContent, 'Max Frankfurter')).toBeTruthy();
+            const message_content = view.el.querySelector('.chat-msg__text');
+            expect(message_content.textContent).toBe(spoiler);
+            const spoiler_hint_el = view.el.querySelector('.spoiler-hint');
+            expect(spoiler_hint_el.textContent).toBe('');
+            done();
         }));
 
         it("can be sent without a hint",
             mock.initConverseWithPromises(
                 null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                function (done, _converse) {
+                async function (done, _converse) {
 
             test_utils.createContacts(_converse, 'current', 1);
             _converse.emit('rosterContactsFetched');
@@ -105,147 +94,145 @@
             // have a resource, that resource is then queried to see
             // whether Strophe.NS.SPOILER is supported, in which case
             // the spoiler button will appear.
-            var presence = $pres({
+            const presence = $pres({
                 'from': contact_jid+'/phone',
                 'to': 'dummy@localhost'
             });
             _converse.connection._dataRecv(test_utils.createRequest(presence));
-            test_utils.openChatBoxFor(_converse, contact_jid)
-            .then(() => test_utils.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]))
-            .then(() => {
-                var view = _converse.chatboxviews.get(contact_jid);
-                spyOn(view, 'onMessageSubmitted').and.callThrough();
-                spyOn(_converse.connection, 'send');
-
-                var spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler');
-                spoiler_toggle.click();
-
-                var textarea = view.el.querySelector('.chat-textarea');
-                textarea.value = 'This is the spoiler';
-                view.keyPressed({
-                    target: textarea,
-                    preventDefault: _.noop,
-                    keyCode: 13
-                });
-                expect(view.onMessageSubmitted).toHaveBeenCalled();
-
-                /* Test the XML stanza 
-                 *
-                 * <message from="dummy@localhost/resource"
-                 *          to="max.frankfurter@localhost"
-                 *          type="chat"
-                 *          id="4547c38b-d98b-45a5-8f44-b4004dbc335e"
-                 *          xmlns="jabber:client">
-                 *    <body>This is the spoiler</body>
-                 *    <active xmlns="http://jabber.org/protocol/chatstates"/>
-                 *    <spoiler xmlns="urn:xmpp:spoiler:0"/>
-                 * </message>"
-                 */
-                var stanza = _converse.connection.send.calls.argsFor(0)[0].tree();
-                var spoiler_el = stanza.querySelector('spoiler[xmlns="urn:xmpp:spoiler:0"]');
-                expect(_.isNull(spoiler_el)).toBeFalsy();
-                expect(spoiler_el.textContent).toBe('');
-
-                var body_el = stanza.querySelector('body');
-                expect(body_el.textContent).toBe('This is the spoiler');
-
-                /* Test the HTML spoiler message */
-                expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Max Mustermann');
-
-                var spoiler_msg_el = view.el.querySelector('.chat-msg__text.spoiler');
-                expect(spoiler_msg_el.textContent).toBe('This is the spoiler');
-                expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
-
-                spoiler_toggle = view.el.querySelector('.spoiler-toggle');
-                expect(spoiler_toggle.textContent).toBe('Show more');
-                spoiler_toggle.click();
-                expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeFalsy();
-                expect(spoiler_toggle.textContent).toBe('Show less');
-                spoiler_toggle.click();
-                expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
-                done();
-            }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+            await test_utils.openChatBoxFor(_converse, contact_jid);
+            await test_utils.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]);
+            const view = _converse.chatboxviews.get(contact_jid);
+            spyOn(view, 'onMessageSubmitted').and.callThrough();
+            spyOn(_converse.connection, 'send');
+
+            let spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler');
+            spoiler_toggle.click();
+
+            const textarea = view.el.querySelector('.chat-textarea');
+            textarea.value = 'This is the spoiler';
+            view.keyPressed({
+                target: textarea,
+                preventDefault: _.noop,
+                keyCode: 13
+            });
+            expect(view.onMessageSubmitted).toHaveBeenCalled();
+            await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+
+            /* Test the XML stanza 
+             *
+             * <message from="dummy@localhost/resource"
+             *          to="max.frankfurter@localhost"
+             *          type="chat"
+             *          id="4547c38b-d98b-45a5-8f44-b4004dbc335e"
+             *          xmlns="jabber:client">
+             *    <body>This is the spoiler</body>
+             *    <active xmlns="http://jabber.org/protocol/chatstates"/>
+             *    <spoiler xmlns="urn:xmpp:spoiler:0"/>
+             * </message>"
+             */
+            const stanza = _converse.connection.send.calls.argsFor(0)[0].tree();
+            const spoiler_el = stanza.querySelector('spoiler[xmlns="urn:xmpp:spoiler:0"]');
+            expect(_.isNull(spoiler_el)).toBeFalsy();
+            expect(spoiler_el.textContent).toBe('');
+
+            const body_el = stanza.querySelector('body');
+            expect(body_el.textContent).toBe('This is the spoiler');
+
+            /* Test the HTML spoiler message */
+            expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Max Mustermann');
+
+            const spoiler_msg_el = view.el.querySelector('.chat-msg__text.spoiler');
+            expect(spoiler_msg_el.textContent).toBe('This is the spoiler');
+            expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
+
+            spoiler_toggle = view.el.querySelector('.spoiler-toggle');
+            expect(spoiler_toggle.textContent).toBe('Show more');
+            spoiler_toggle.click();
+            expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeFalsy();
+            expect(spoiler_toggle.textContent).toBe('Show less');
+            spoiler_toggle.click();
+            expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
+            done();
         }));
 
         it("can be sent with a hint",
             mock.initConverseWithPromises(
                 null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                function (done, _converse) {
+                async function (done, _converse) {
 
             test_utils.createContacts(_converse, 'current', 1);
             _converse.emit('rosterContactsFetched');
 
             test_utils.openControlBox();
-            var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
 
             // XXX: We need to send a presence from the contact, so that we
             // have a resource, that resource is then queried to see
             // whether Strophe.NS.SPOILER is supported, in which case
             // the spoiler button will appear.
-            var presence = $pres({
+            const presence = $pres({
                 'from': contact_jid+'/phone',
                 'to': 'dummy@localhost'
             });
             _converse.connection._dataRecv(test_utils.createRequest(presence));
-            test_utils.openChatBoxFor(_converse, contact_jid)
-            .then(() => test_utils.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]))
-            .then(() => {
-                var view = _converse.chatboxviews.get(contact_jid);
-                var spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler');
-                spoiler_toggle.click();
-
-                spyOn(view, 'onMessageSubmitted').and.callThrough();
-                spyOn(_converse.connection, 'send');
-
-                var textarea = view.el.querySelector('.chat-textarea');
-                textarea.value = 'This is the spoiler';
-                var hint_input = view.el.querySelector('.spoiler-hint');
-                hint_input.value = 'This is the hint';
-
-                view.keyPressed({
-                    target: textarea,
-                    preventDefault: _.noop,
-                    keyCode: 13
-                });
-                expect(view.onMessageSubmitted).toHaveBeenCalled();
-
-                /* Test the XML stanza 
-                 *
-                 * <message from="dummy@localhost/resource"
-                 *          to="max.frankfurter@localhost"
-                 *          type="chat"
-                 *          id="4547c38b-d98b-45a5-8f44-b4004dbc335e"
-                 *          xmlns="jabber:client">
-                 *    <body>This is the spoiler</body>
-                 *    <active xmlns="http://jabber.org/protocol/chatstates"/>
-                 *    <spoiler xmlns="urn:xmpp:spoiler:0">This is the hint</spoiler>
-                 * </message>"
-                 */
-                var stanza = _converse.connection.send.calls.argsFor(0)[0].tree();
-                var spoiler_el = stanza.querySelector('spoiler[xmlns="urn:xmpp:spoiler:0"]');
-
-                expect(_.isNull(spoiler_el)).toBeFalsy();
-                expect(spoiler_el.textContent).toBe('This is the hint');
-
-                var body_el = stanza.querySelector('body');
-                expect(body_el.textContent).toBe('This is the spoiler');
-
-                /* Test the HTML spoiler message */
-                expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Max Mustermann');
-
-                var spoiler_msg_el = view.el.querySelector('.chat-msg__text.spoiler');
-                expect(spoiler_msg_el.textContent).toBe('This is the spoiler');
-                expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
-
-                spoiler_toggle = view.el.querySelector('.spoiler-toggle');
-                expect(spoiler_toggle.textContent).toBe('Show more');
-                spoiler_toggle.click();
-                expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeFalsy();
-                expect(spoiler_toggle.textContent).toBe('Show less');
-                spoiler_toggle.click();
-                expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
-                done();
+            await test_utils.openChatBoxFor(_converse, contact_jid);
+            await test_utils.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]);
+            const view = _converse.chatboxviews.get(contact_jid);
+            let spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler');
+            spoiler_toggle.click();
+
+            spyOn(view, 'onMessageSubmitted').and.callThrough();
+            spyOn(_converse.connection, 'send');
+
+            const textarea = view.el.querySelector('.chat-textarea');
+            textarea.value = 'This is the spoiler';
+            const hint_input = view.el.querySelector('.spoiler-hint');
+            hint_input.value = 'This is the hint';
+
+            view.keyPressed({
+                target: textarea,
+                preventDefault: _.noop,
+                keyCode: 13
             });
+            expect(view.onMessageSubmitted).toHaveBeenCalled();
+            await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+
+            /* Test the XML stanza 
+             *
+             * <message from="dummy@localhost/resource"
+             *          to="max.frankfurter@localhost"
+             *          type="chat"
+             *          id="4547c38b-d98b-45a5-8f44-b4004dbc335e"
+             *          xmlns="jabber:client">
+             *    <body>This is the spoiler</body>
+             *    <active xmlns="http://jabber.org/protocol/chatstates"/>
+             *    <spoiler xmlns="urn:xmpp:spoiler:0">This is the hint</spoiler>
+             * </message>"
+             */
+            const stanza = _converse.connection.send.calls.argsFor(0)[0].tree();
+            const spoiler_el = stanza.querySelector('spoiler[xmlns="urn:xmpp:spoiler:0"]');
+
+            expect(_.isNull(spoiler_el)).toBeFalsy();
+            expect(spoiler_el.textContent).toBe('This is the hint');
+
+            const body_el = stanza.querySelector('body');
+            expect(body_el.textContent).toBe('This is the spoiler');
+
+            /* Test the HTML spoiler message */
+            expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Max Mustermann');
+
+            const spoiler_msg_el = view.el.querySelector('.chat-msg__text.spoiler');
+            expect(spoiler_msg_el.textContent).toBe('This is the spoiler');
+            expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
+
+            spoiler_toggle = view.el.querySelector('.spoiler-toggle');
+            expect(spoiler_toggle.textContent).toBe('Show more');
+            spoiler_toggle.click();
+            expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeFalsy();
+            expect(spoiler_toggle.textContent).toBe('Show less');
+            spoiler_toggle.click();
+            expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
+            done();
         }));
     });
 }));

+ 2 - 2
src/converse-chatboxes.js

@@ -297,7 +297,7 @@
                             'message': _converse.chatboxes.getMessageBody(stanza),
                             'references': this.getReferencesFromStanza(stanza),
                             'older_versions': older_versions,
-                            'edited': true
+                            'edited': moment().format()
                         });
                         return true;
                     }
@@ -395,7 +395,7 @@
                         older_versions.push(message.get('message'));
                         message.save({
                             'correcting': false,
-                            'edited': true,
+                            'edited': moment().format(),
                             'message': attrs.message,
                             'older_versions': older_versions,
                             'references': attrs.references

+ 20 - 15
src/converse-chatboxviews.js

@@ -17,8 +17,10 @@
     const { Backbone, _ } = converse.env;
 
     const AvatarMixin = {
-        renderAvatar () {
-            const canvas_el = this.el.querySelector('canvas');
+
+        renderAvatar (el) {
+            el = el || this.el;
+            const canvas_el = el.querySelector('canvas');
             if (_.isNull(canvas_el)) {
                 return;
             }
@@ -27,19 +29,22 @@
                     img_src = "data:" + image_type + ";base64," + image,
                     img = new Image();
 
-            img.onload = () => {
-                const ctx = canvas_el.getContext('2d'),
-                        ratio = img.width / img.height;
-                ctx.clearRect(0, 0, canvas_el.width, canvas_el.height);
-                if (ratio < 1) {
-                    const scaled_img_with = canvas_el.width*ratio,
-                            x = Math.floor((canvas_el.width-scaled_img_with)/2);
-                    ctx.drawImage(img, x, 0, scaled_img_with, canvas_el.height);
-                } else {
-                    ctx.drawImage(img, 0, 0, canvas_el.width, canvas_el.height*ratio);
-                }
-            };
-            img.src = img_src;
+            return new Promise((resolve, reject) => {
+                img.onload = () => {
+                    const ctx = canvas_el.getContext('2d'),
+                            ratio = img.width / img.height;
+                    ctx.clearRect(0, 0, canvas_el.width, canvas_el.height);
+                    if (ratio < 1) {
+                        const scaled_img_with = canvas_el.width*ratio,
+                                x = Math.floor((canvas_el.width-scaled_img_with)/2);
+                        ctx.drawImage(img, x, 0, scaled_img_with, canvas_el.height);
+                    } else {
+                        ctx.drawImage(img, 0, 0, canvas_el.width, canvas_el.height*ratio);
+                    }
+                    resolve();
+                };
+                img.src = img_src;
+            });
         },
     };
 

+ 7 - 2
src/converse-chatview.js

@@ -312,6 +312,7 @@
 
                     this.model.messages.on('add', this.onMessageAdded, this);
                     this.model.messages.on('rendered', this.scrollDown, this);
+                    this.model.messages.on('edited', (view) => this.markFollowups(view.el));
 
                     this.model.on('show', this.show, this);
                     this.model.on('destroy', this.remove, this);
@@ -673,7 +674,8 @@
                     if (view.model.get('type') === 'error') {
                         const previous_msg_el = this.content.querySelector(`[data-msgid="${view.model.get('msgid')}"]`);
                         if (previous_msg_el) {
-                            return previous_msg_el.insertAdjacentElement('afterend', view.el);
+                            previous_msg_el.insertAdjacentElement('afterend', view.el);
+                            return this.trigger('messageInserted', view.el);
                         }
                     }
                     const current_msg_date = moment(view.model.get('time')) || moment,
@@ -692,6 +694,7 @@
                         previous_msg_el.insertAdjacentElement('afterend', view.el);
                         this.markFollowups(view.el);
                     }
+                    return this.trigger('messageInserted', view.el);
                 },
 
                 markFollowups (el) {
@@ -731,7 +734,7 @@
                     }
                 },
 
-                showMessage (message) {
+                async showMessage (message) {
                     /* Inserts a chat message into the content area of the chat box.
                      *
                      * Will also insert a new day indicator if the message is on a
@@ -741,6 +744,8 @@
                      *  (Backbone.Model) message: The message object
                      */
                     const view = new _converse.MessageView({'model': message});
+                    await view.render();
+                    
                     this.clearChatStateNotification(message);
                     this.insertMessage(view);
                     this.insertDayIndicator(view.el);

+ 34 - 25
src/converse-message-view.js

@@ -41,8 +41,11 @@
                 { __ } = _converse;
 
 
-            _converse.MessageVersionsModal = _converse.BootstrapModal.extend({
+            _converse.api.settings.update({
+                'show_images_inline': true
+            });
 
+            _converse.MessageVersionsModal = _converse.BootstrapModal.extend({
                 toHTML () {
                     return tpl_message_versions_modal(_.extend(
                         this.model.toJSON(), {
@@ -61,17 +64,11 @@
                     if (this.model.vcard) {
                         this.model.vcard.on('change', this.render, this);
                     }
-                    this.model.on('change:correcting', this.onMessageCorrection, this);
-                    this.model.on('change:message', this.render, this);
-                    this.model.on('change:progress', this.renderFileUploadProgresBar, this);
-                    this.model.on('change:type', this.render, this);
-                    this.model.on('change:upload', this.render, this);
+                    this.model.on('change', this.onChanged, this);
                     this.model.on('destroy', this.remove, this);
-                    this.render();
                 },
 
-                render () {
-                    const is_followup = u.hasClass('chat-msg--followup', this.el);
+                async render () {
                     let msg;
                     if (this.model.isOnlyChatStateNotification()) {
                         this.renderChatStateNotification()
@@ -80,22 +77,34 @@
                     } else if (this.model.get('type') === 'error') {
                         this.renderErrorMessage();
                     } else {
-                        this.renderChatMessage();
-                    }
-                    if (is_followup) {
-                        u.addClass('chat-msg--followup', this.el);
+                        await this.renderChatMessage();
                     }
                     return this.el;
                 },
 
-                onMessageCorrection () {
-                    this.render();
-                    if (!this.model.get('correcting') && this.model.changed.message) {
-                        this.el.addEventListener('animationend', () => u.removeClass('onload', this.el));
-                        u.addClass('onload', this.el);
+                async onChanged (item) {
+                    // Jot down whether it was edited because the `changed`
+                    // attr gets removed when this.render() gets called further
+                    // down.
+                    const edited = item.changed.edited;
+                    if (this.model.changed.progress) {
+                        return this.renderFileUploadProgresBar();
+                    }
+                    if (_.filter(['correcting', 'message', 'type', 'upload'],
+                                 prop => Object.prototype.hasOwnProperty.call(this.model.changed, prop)).length) {
+                        await this.render();
+                    }
+                    if (edited) {
+                        this.onMessageEdited();
                     }
                 },
 
+                onMessageEdited () {
+                    this.el.addEventListener('animationend', () => u.removeClass('onload', this.el));
+                    this.model.collection.trigger('edited', this);
+                    u.addClass('onload', this.el);
+                },
+
                 replaceElement (msg) {
                     if (!_.isNil(this.el.parentElement)) {
                         this.el.parentElement.replaceChild(msg, this.el);
@@ -104,7 +113,7 @@
                     return this.el;
                 },
 
-                renderChatMessage () {
+                async renderChatMessage () {
                     const is_me_message = this.isMeCommand(),
                           moment_time = moment(this.model.get('time')),
                           role = this.model.vcard ? this.model.vcard.get('role') : null,
@@ -148,14 +157,14 @@
                             _.partial(u.addEmoji, _converse, _)
                         )(text);
                     }
-                    u.renderImageURLs(_converse, msg_content).then(() => {
-                        this.model.collection.trigger('rendered');
-                    });
-                    this.replaceElement(msg);
-
+                    if (_converse.show_images_inline) {
+                        await u.renderImageURLs(_converse, msg_content);
+                    }
                     if (this.model.get('type') !== 'headline') {
-                        this.renderAvatar();
+                        await this.renderAvatar(msg);
                     }
+                    this.replaceElement(msg);
+                    this.model.collection.trigger('rendered', this);
                 },
 
                 renderErrorMessage () {

+ 1 - 0
src/converse-muc-views.js

@@ -478,6 +478,7 @@
                 initialize () {
                     _converse.BootstrapModal.prototype.initialize.apply(this, arguments);
                     this.model.on('change', this.render, this);
+                    this.model.occupants.on('add', this.render, this);
                     this.model.occupants.on('change', this.render, this);
                 },
 

+ 1 - 1
src/utils/core.js

@@ -71,7 +71,7 @@
         'warn': _.get(console, 'log') ? console.log.bind(console) : _.noop
     }, console);
 
-    var isImage = function (url) {
+    const isImage = function (url) {
         return new Promise((resolve, reject) => {
             var img = new Image();
             var timer = window.setTimeout(function () {

+ 6 - 4
tests/utils.js

@@ -297,13 +297,15 @@
                .c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree();
     }
 
-    utils.sendMessage = function (chatboxview, message) {
-        chatboxview.el.querySelector('.chat-textarea').value = message;
-        chatboxview.keyPressed({
-            target: chatboxview.el.querySelector('textarea.chat-textarea'),
+    utils.sendMessage = function (view, message) {
+        const promise = new Promise((resolve, reject) => view.on('messageInserted', resolve));
+        view.el.querySelector('.chat-textarea').value = message;
+        view.keyPressed({
+            target: view.el.querySelector('textarea.chat-textarea'),
             preventDefault: _.noop,
             keyCode: 13
         });
+        return promise;
     };
     return utils;
 }));

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