Browse Source

Use our own `confirm` dialog consistently

JC Brand 3 năm trước cách đây
mục cha
commit
a57853156e

+ 1 - 0
karma.conf.js

@@ -12,6 +12,7 @@ module.exports = function(config) {
       { pattern: 'dist/*.css.map', included: false },
       { pattern: "dist/icons.js", served: true },
       { pattern: "dist/emojis.js", served: true },
+      "src/shared/tests/tests.css",
       "node_modules/lodash/lodash.min.js",
       "dist/converse.js",
       "dist/converse.css",

+ 3 - 3
src/headless/plugins/muc/utils.js

@@ -102,9 +102,9 @@ export async function onDirectMUCInvitation (message) {
         let contact = _converse.roster.get(from);
         contact = contact ? contact.getDisplayName() : from;
         if (!reason) {
-            result = confirm(__('%1$s has invited you to join a groupchat: %2$s', contact, room_jid));
+            result = await api.confirm(__('%1$s has invited you to join a groupchat: %2$s', contact, room_jid));
         } else {
-            result = confirm(
+            result = await api.confirm(
                 __(
                     '%1$s has invited you to join a groupchat: %2$s, and left the following reason: "%3$s"',
                     contact,
@@ -114,7 +114,7 @@ export async function onDirectMUCInvitation (message) {
             );
         }
     }
-    if (result === true) {
+    if (result) {
         const chatroom = await openChatRoom(room_jid, { 'password': x_el.getAttribute('password') });
         if (chatroom.session.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED) {
             _converse.chatboxes.get(room_jid).rejoin();

+ 3 - 2
src/modals/tests/user-details-modal.js

@@ -19,7 +19,8 @@ describe("The User Details Modal", function () {
         show_modal_button.click();
         const modal = _converse.api.modal.get('user-details-modal');
         await u.waitUntil(() => u.isVisible(modal.el), 1000);
-        spyOn(window, 'confirm').and.returnValue(true);
+        spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
+
         spyOn(view.model.contact, 'removeFromRoster').and.callFake(callback => callback());
         let remove_contact_button = modal.el.querySelector('button.remove-contact');
         expect(u.isVisible(remove_contact_button)).toBeTruthy();
@@ -45,7 +46,7 @@ describe("The User Details Modal", function () {
         show_modal_button.click();
         let modal = _converse.api.modal.get('user-details-modal');
         await u.waitUntil(() => u.isVisible(modal.el), 2000);
-        spyOn(window, 'confirm').and.returnValue(true);
+        spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
 
         spyOn(view.model.contact, 'removeFromRoster').and.callFake((callback, errback) => errback());
         let remove_contact_button = modal.el.querySelector('button.remove-contact');

+ 3 - 3
src/modals/user-details.js

@@ -83,11 +83,11 @@ const UserDetailsModal = BootstrapModal.extend({
         u.removeClass('fa-spin', refresh_icon);
     },
 
-    removeContact (ev) {
+    async removeContact (ev) {
         ev?.preventDefault?.();
         if (!api.settings.get('allow_contact_removal')) { return; }
-        const result = confirm(__("Are you sure you want to remove this contact?"));
-        if (result === true) {
+        const result = await api.confirm(__("Are you sure you want to remove this contact?"));
+        if (result) {
             // XXX: The `dismissHandler` in bootstrap.native tries to
             // reference the remove button after it's been cleared from
             // the DOM, so we delay removing the contact to give it time.

+ 2 - 2
src/plugins/bookmark-views/tests/bookmarks.js

@@ -557,9 +557,9 @@ describe("Bookmarks", function () {
             expect(els[3].textContent).toBe("noname@conference.shakespeare.lit");
             expect(els[4].textContent).toBe("The Play's the Thing");
 
-            spyOn(window, 'confirm').and.returnValue(true);
+            spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
             document.querySelector('#chatrooms .bookmarks.rooms-list .room-item:nth-child(2) a:nth-child(2)').click();
-            expect(window.confirm).toHaveBeenCalled();
+            expect(_converse.api.confirm).toHaveBeenCalled();
             await u.waitUntil(() => document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item').length === 4)
             els = document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item a.list-item-link');
             expect(els[0].textContent).toBe("1st Bookmark");

+ 3 - 2
src/plugins/bookmark-views/utils.js

@@ -25,11 +25,12 @@ export function getHeadingButtons (view, buttons) {
     return buttons;
 }
 
-export function removeBookmarkViaEvent (ev) {
+export async function removeBookmarkViaEvent (ev) {
     ev.preventDefault();
     const name = ev.target.getAttribute('data-bookmark-name');
     const jid = ev.target.getAttribute('data-room-jid');
-    if (confirm(__('Are you sure you want to remove the bookmark "%1$s"?', name))) {
+    const result = await api.confirm(__('Are you sure you want to remove the bookmark "%1$s"?', name));
+    if (result) {
         invokeMap(_converse.bookmarks.where({ jid }), Model.prototype.destroy);
     }
 }

+ 5 - 5
src/plugins/chatview/tests/chatbox.js

@@ -49,7 +49,7 @@ describe("Chatboxes", function () {
             const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             await mock.openChatBoxFor(_converse, contact_jid);
             const view = _converse.chatboxviews.get(contact_jid);
-            spyOn(window, 'confirm').and.returnValue(true);
+            spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
 
             for (const i of Array(10).keys()) {
                 mock.sendMessage(view, `Message ${i}`);
@@ -64,7 +64,7 @@ describe("Chatboxes", function () {
                 preventDefault: function preventDefault () {},
                 keyCode: 13 // Enter
             });
-            await u.waitUntil(() => window.confirm.calls.count() === 1);
+            await u.waitUntil(() => _converse.api.confirm.calls.count() === 1);
             await u.waitUntil(() => sizzle('converse-chat-message', view).length === 0);
             expect(true).toBe(true);
         }));
@@ -916,15 +916,15 @@ describe("Chatboxes", function () {
 
             message = '/clear';
             const message_form = view.querySelector('converse-message-form');
-            spyOn(window, 'confirm').and.callFake(() => true);
+            spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
             view.querySelector('.chat-textarea').value = message;
             message_form.onKeyDown({
                 target: view.querySelector('textarea.chat-textarea'),
                 preventDefault: function preventDefault () {},
                 keyCode: 13
             });
-            await u.waitUntil(() => window.confirm.calls.count() === 1);
-            expect(window.confirm).toHaveBeenCalledWith('Are you sure you want to clear the messages from this conversation?');
+            await u.waitUntil(() => _converse.api.confirm.calls.count() === 1);
+            expect(_converse.api.confirm).toHaveBeenCalledWith('Are you sure you want to clear the messages from this conversation?');
             await u.waitUntil(() => view.model.messages.length === 0);
             await u.waitUntil(() => !view.querySelectorAll('.chat-msg__body').length);
         }));

+ 8 - 5
src/plugins/chatview/tests/corrections.js

@@ -268,23 +268,26 @@ describe("A Chat Message", function () {
         expect(view.querySelectorAll('.chat-msg .chat-msg__action').length).toBe(2);
 
         // Test confirmation dialog
-        spyOn(window, 'confirm').and.returnValue(true);
+        spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
         textarea.value = 'But soft, what light through yonder airlock breaks?';
         action = view.querySelector('.chat-msg .chat-msg__action');
         action.style.opacity = 1;
         action.click();
-        expect(window.confirm).toHaveBeenCalledWith(
+
+        await u.waitUntil(() => _converse.api.confirm.calls.count());
+        expect(_converse.api.confirm).toHaveBeenCalledWith(
             'You have an unsent message which will be lost if you continue. Are you sure?');
         expect(view.model.messages.at(0).get('correcting')).toBe(true);
         expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
 
         textarea.value = 'But soft, what light through yonder airlock breaks?'
         action.click();
+
+        await u.waitUntil(() => _converse.api.confirm.calls.count() === 2);
         expect(view.model.messages.at(0).get('correcting')).toBe(false);
-        expect(window.confirm.calls.count()).toBe(2);
-        expect(window.confirm.calls.argsFor(0)).toEqual(
+        expect(_converse.api.confirm.calls.argsFor(0)).toEqual(
             ['You have an unsent message which will be lost if you continue. Are you sure?']);
-        expect(window.confirm.calls.argsFor(1)).toEqual(
+        expect(_converse.api.confirm.calls.argsFor(1)).toEqual(
             ['You have an unsent message which will be lost if you continue. Are you sure?']);
     }));
 

+ 2 - 2
src/plugins/chatview/utils.js

@@ -31,8 +31,8 @@ export async function getHeadingStandaloneButton (promise_or_data) {
 }
 
 export async function clearMessages (chat) {
-    const result = confirm(__('Are you sure you want to clear the messages from this conversation?'));
-    if (result === true) {
+    const result = await api.confirm(__('Are you sure you want to clear the messages from this conversation?'));
+    if (result) {
         await chat.clearMessages();
     }
 }

+ 5 - 5
src/plugins/muc-views/tests/muc.js

@@ -1385,7 +1385,7 @@ describe("Groupchats", function () {
             const from_jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
             await u.waitUntil(() => _converse.roster.get(from_jid).vcard.get('fullname'));
 
-            spyOn(window, 'confirm').and.callFake(() => true);
+            spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
             const view = _converse.chatboxviews.get('lounge@montague.lit');
             await view.close(); // Hack, otherwise we have to mock stanzas.
@@ -1402,7 +1402,7 @@ describe("Groupchats", function () {
                 </message>`);
             await _converse.onDirectMUCInvitation(stanza);
 
-            expect(window.confirm).toHaveBeenCalledWith(
+            expect(_converse.api.confirm).toHaveBeenCalledWith(
                 name + ' has invited you to join a groupchat: '+ muc_jid +
                 ', and left the following reason: "'+reason+'"');
             expect(_converse.chatboxes.models.length).toBe(2);
@@ -2461,15 +2461,15 @@ describe("Groupchats", function () {
             const view = _converse.chatboxviews.get('lounge@montague.lit');
             const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
             textarea.value = '/clear';
-            spyOn(window, 'confirm').and.callFake(() => false);
+            spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(false));
             const message_form = view.querySelector('converse-muc-message-form');
             message_form.onKeyDown({
                 target: textarea,
                 preventDefault: function preventDefault () {},
                 keyCode: 13
             });
-            await u.waitUntil(() => window.confirm.calls.count() === 1);
-            expect(window.confirm).toHaveBeenCalledWith('Are you sure you want to clear the messages from this conversation?');
+            await u.waitUntil(() => _converse.api.confirm.calls.count() === 1);
+            expect(_converse.api.confirm).toHaveBeenCalledWith('Are you sure you want to clear the messages from this conversation?');
         }));
 
         it("takes /owner to make a user an owner", mock.initConverse([], {}, async function (_converse) {

+ 2 - 2
src/plugins/muc-views/tests/muclist.js

@@ -313,7 +313,7 @@ describe("A groupchat shown in the groupchats list", function () {
             async function (_converse) {
 
         const u = converse.env.utils;
-        spyOn(window, 'confirm').and.callFake(() => true);
+        spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
         expect(_converse.chatboxes.length).toBe(1);
         await mock.waitForRoster(_converse, 'current', 0);
         await mock.openChatRoom(_converse, 'lounge', 'conference.shakespeare.lit', 'JC');
@@ -328,7 +328,7 @@ describe("A groupchat shown in the groupchats list", function () {
         const rooms_list = document.querySelector('converse-rooms-list');
         const close_el = rooms_list.querySelector(".close-room");
         close_el.click();
-        expect(window.confirm).toHaveBeenCalledWith(
+        expect(_converse.api.confirm).toHaveBeenCalledWith(
             'Are you sure you want to leave the groupchat lounge@conference.shakespeare.lit?');
 
         await u.waitUntil(() => rooms_list.querySelectorAll(".open-room").length === 0);

+ 6 - 3
src/plugins/omemo/profile.js

@@ -60,10 +60,13 @@ export class Profile extends CustomElement {
 
     async generateOMEMODeviceBundle (ev) {
         ev.preventDefault();
-        if (confirm(__(
+
+        const result = await api.confirm(__(
             'Are you sure you want to generate new OMEMO keys? ' +
-            'This will remove your old keys and all previously encrypted messages will no longer be decryptable on this device.'
-        ))) {
+            'This will remove your old keys and all previously ' +
+            'encrypted messages will no longer be decryptable on this device.'));
+
+        if (result) {
             await api.omemo.bundle.generate();
             await this.setAttributes();
             this.requestUpdate();

+ 2 - 1
src/plugins/roomslist/view.js

@@ -74,7 +74,8 @@ export class RoomsList extends CustomElement {
     async closeRoom (ev) { // eslint-disable-line class-methods-use-this
         ev.preventDefault();
         const name = ev.target.getAttribute('data-room-name');
-        if (confirm(__("Are you sure you want to leave the groupchat %1$s?", name))) {
+        const result = await api.confirm(__("Are you sure you want to leave the groupchat %1$s?", name));
+        if (result) {
             const jid = ev.target.getAttribute('data-room-jid');
             const room = await api.rooms.get(jid);
             room.close();

+ 7 - 5
src/plugins/rosterview/contactview.js

@@ -77,10 +77,12 @@ export default class RosterContact extends CustomElement {
         this.model.openChat();
     }
 
-    removeContact (ev) {
+    async removeContact (ev) {
         ev?.preventDefault?.();
         if (!api.settings.get('allow_contact_removal')) { return; }
-        if (!confirm(__("Are you sure you want to remove this contact?"))) { return; }
+
+        const result = await api.confirm(__("Are you sure you want to remove this contact?"));
+        if (!result)  return;
 
         try {
             this.model.removeFromRoster();
@@ -108,10 +110,10 @@ export default class RosterContact extends CustomElement {
         this.model.authorize().subscribe();
     }
 
-    declineRequest (ev) {
+    async declineRequest (ev) {
         if (ev && ev.preventDefault) { ev.preventDefault(); }
-        const result = confirm(__("Are you sure you want to decline this contact request?"));
-        if (result === true) {
+        const result = await api.confirm(__("Are you sure you want to decline this contact request?"));
+        if (result) {
             this.model.unauthorize().destroy();
         }
         return this;

+ 6 - 6
src/plugins/rosterview/tests/protocol.js

@@ -447,7 +447,7 @@ describe("The Protocol", function () {
             const jid = 'abram@montague.lit';
             await mock.openControlBox(_converse);
             await mock.waitForRoster(_converse, 'current');
-            spyOn(window, 'confirm').and.returnValue(true);
+            spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
             // We now have a contact we want to remove
             expect(_converse.roster.get(jid) instanceof _converse.RosterContact).toBeTruthy();
 
@@ -457,7 +457,7 @@ describe("The Protocol", function () {
 
             // remove the first user
             header.parentElement.querySelector('li .remove-xmpp-contact').click();
-            expect(window.confirm).toHaveBeenCalled();
+            expect(_converse.api.confirm).toHaveBeenCalled();
 
             /* Section 8.6 Removing a Roster Item and Cancelling All
              * Subscriptions
@@ -478,14 +478,14 @@ describe("The Protocol", function () {
              *   </query>
              * </iq>
              */
-            const sent_iq = _converse.connection.IQ_stanzas.pop();
-
-            expect(Strophe.serialize(sent_iq)).toBe(
-                `<iq id="${sent_iq.getAttribute('id')}" type="set" xmlns="jabber:client">`+
+            const iq_stanzas = _converse.connection.IQ_stanzas;
+            await u.waitUntil(() => Strophe.serialize(iq_stanzas.at(-1)) ===
+                `<iq id="${iq_stanzas.at(-1).getAttribute('id')}" type="set" xmlns="jabber:client">`+
                     `<query xmlns="jabber:iq:roster">`+
                         `<item jid="abram@montague.lit" subscription="remove"/>`+
                     `</query>`+
                 `</iq>`);
+            const sent_iq = iq_stanzas.at(-1);
 
             // Receive confirmation from the contact's server
             // <iq type='result' id='remove1'/>

+ 23 - 22
src/plugins/rosterview/tests/roster.js

@@ -642,7 +642,7 @@ describe("The Contacts Roster", function () {
             const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const contact = _converse.roster.get(jid);
             var sent_IQ;
-            spyOn(window, 'confirm').and.returnValue(true);
+            spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
             spyOn(contact, 'unauthorize').and.callFake(function () { return contact; });
             spyOn(contact, 'removeFromRoster').and.callThrough();
             const rosterview = document.querySelector('converse-roster');
@@ -653,7 +653,7 @@ describe("The Contacts Roster", function () {
             });
             sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, rosterview).pop().click();
             await u.waitUntil(() => (sizzle(".pending-contact-name:contains('"+name+"')", rosterview).length === 0), 1000);
-            expect(window.confirm).toHaveBeenCalled();
+            expect(_converse.api.confirm).toHaveBeenCalled();
             expect(contact.removeFromRoster).toHaveBeenCalled();
             expect(Strophe.serialize(sent_IQ)).toBe(
                 `<iq type="set" xmlns="jabber:client">`+
@@ -684,18 +684,19 @@ describe("The Contacts Roster", function () {
             }, 700)
 
             const remove_el = await u.waitUntil(() => sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, rosterview).pop());
-            spyOn(window, 'confirm').and.returnValue(true);
+            spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
             remove_el.click();
-            expect(window.confirm).toHaveBeenCalled();
+            expect(_converse.api.confirm).toHaveBeenCalled();
 
-            const iq = _converse.connection.IQ_stanzas.pop();
-            expect(Strophe.serialize(iq)).toBe(
-                `<iq id="${iq.getAttribute('id')}" type="set" xmlns="jabber:client">`+
+            const iq_stanzas = _converse.connection.IQ_stanzas;
+            await u.waitUntil(() => Strophe.serialize(iq_stanzas.at(-1)) ===
+                `<iq id="${iq_stanzas.at(-1).getAttribute('id')}" type="set" xmlns="jabber:client">`+
                     `<query xmlns="jabber:iq:roster">`+
                         `<item jid="lord.capulet@montague.lit" subscription="remove"/>`+
                     `</query>`+
                 `</iq>`);
 
+            const iq = iq_stanzas.at(-1);
             const stanza = u.toStanza(`<iq id="${iq.getAttribute('id')}" to="romeo@montague.lit/orchard" type="result"/>`);
             _converse.connection._dataRecv(mock.createRequest(stanza));
             await u.waitUntil(() => rosterview.querySelector(`ul[data-group="Pending contacts"]`) === null);
@@ -709,7 +710,7 @@ describe("The Contacts Roster", function () {
             await Promise.all(_converse.roster.map(contact => u.waitUntil(() => contact.vcard.get('fullname'))));
             await u.waitUntil(() => _converse.roster.at(0).vcard.get('fullname'))
             const rosterview = document.querySelector('converse-roster');
-            spyOn(window, 'confirm').and.returnValue(true);
+            spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
             for (let i=0; i<mock.pend_names.length; i++) {
                 const name = mock.pend_names[i];
                 sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, rosterview).pop().click();
@@ -824,16 +825,18 @@ describe("The Contacts Roster", function () {
             const name = mock.cur_names[0];
             const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const contact = _converse.roster.get(jid);
-            spyOn(window, 'confirm').and.returnValue(true);
+            spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
             spyOn(contact, 'removeFromRoster').and.callThrough();
 
             let sent_IQ;
-            spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback) {
+            spyOn(_converse.connection, 'sendIQ').and.callFake((iq, callback) => {
                 sent_IQ = iq;
                 callback();
             });
             sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, rosterview).pop().click();
-            expect(window.confirm).toHaveBeenCalled();
+            expect(_converse.api.confirm).toHaveBeenCalled();
+            await u.waitUntil(() => sent_IQ);
+
             expect(Strophe.serialize(sent_IQ)).toBe(
                 `<iq type="set" xmlns="jabber:client">`+
                     `<query xmlns="jabber:iq:roster"><item jid="mercutio@montague.lit" subscription="remove"/></query>`+
@@ -858,15 +861,13 @@ describe("The Contacts Roster", function () {
             });
             const rosterview = document.querySelector('converse-roster');
             await u.waitUntil(() => sizzle('.roster-group', rosterview).filter(u.isVisible).map(e => e.querySelector('li')).length, 1000);
-            spyOn(window, 'confirm').and.returnValue(true);
+            spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
             spyOn(contact, 'removeFromRoster').and.callThrough();
-            spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback) {
-                if (typeof callback === "function") { return callback(); }
-            });
+            spyOn(_converse.connection, 'sendIQ').and.callFake((iq, callback) => callback?.());
             expect(u.isVisible(rosterview.querySelector('.roster-group'))).toBe(true);
             sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, rosterview).pop().click();
-            expect(window.confirm).toHaveBeenCalled();
-            expect(_converse.connection.sendIQ).toHaveBeenCalled();
+            expect(_converse.api.confirm).toHaveBeenCalled();
+            await u.waitUntil(() => _converse.connection.sendIQ.calls.count());
             expect(contact.removeFromRoster).toHaveBeenCalled();
             await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length === 0);
         }));
@@ -1126,7 +1127,7 @@ describe("The Contacts Roster", function () {
             await mock.openControlBox(_converse);
             await mock.waitForRoster(_converse, "current", 0);
             const name = mock.req_names[0];
-            spyOn(window, 'confirm').and.returnValue(true);
+            spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
             _converse.roster.create({
                 'jid': name.replace(/ /g,'.').toLowerCase() + '@montague.lit',
                 'subscription': 'none',
@@ -1139,7 +1140,7 @@ describe("The Contacts Roster", function () {
             expect(u.isVisible(rosterview.querySelector(`ul[data-group="Contact requests"]`))).toEqual(true);
             expect(sizzle('.roster-group', rosterview).filter(u.isVisible).map(e => e.querySelector('li')).length).toBe(1);
             sizzle('.roster-group', rosterview).filter(u.isVisible).map(e => e.querySelector('li .decline-xmpp-request'))[0].click();
-            expect(window.confirm).toHaveBeenCalled();
+            expect(_converse.api.confirm).toHaveBeenCalled();
             await u.waitUntil(() => rosterview.querySelector(`ul[data-group="Contact requests"]`) === null);
         }));
 
@@ -1191,12 +1192,12 @@ describe("The Contacts Roster", function () {
             const name = mock.req_names.sort()[1];
             const jid =  name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const contact = _converse.roster.get(jid);
-            spyOn(window, 'confirm').and.returnValue(true);
+            spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
             spyOn(contact, 'unauthorize').and.callFake(function () { return contact; });
             const req_contact = await u.waitUntil(() => sizzle(".req-contact-name:contains('"+name+"')", rosterview).pop());
             req_contact.parentElement.parentElement.querySelector('.decline-xmpp-request').click();
-            expect(window.confirm).toHaveBeenCalled();
-            expect(contact.unauthorize).toHaveBeenCalled();
+            expect(_converse.api.confirm).toHaveBeenCalled();
+            await u.waitUntil(() => contact.unauthorize.calls.count());
             // There should now be one less contact
             expect(_converse.roster.length).toEqual(mock.req_names.length-1);
         }));

+ 3 - 4
src/shared/chat/message-actions.js

@@ -65,16 +65,15 @@ class MessageActions extends CustomElement {
         `;
     }
 
-    onMessageEditButtonClicked (ev) {
+    async onMessageEditButtonClicked (ev) {
         ev.preventDefault();
         const currently_correcting = this.model.collection.findWhere('correcting');
         // TODO: Use state intead of DOM querying
         // Then this code can also be put on the model
         const unsent_text = u.ancestor(this, '.chatbox')?.querySelector('.chat-textarea')?.value;
         if (unsent_text && (!currently_correcting || currently_correcting.getMessageText() !== unsent_text)) {
-            if (!confirm(__('You have an unsent message which will be lost if you continue. Are you sure?'))) {
-                return;
-            }
+            const result = await api.confirm(__('You have an unsent message which will be lost if you continue. Are you sure?'));
+            if (!result) return;
         }
         if (currently_correcting !== this.model) {
             currently_correcting?.save('correcting', false);

+ 3 - 0
src/shared/tests/tests.css

@@ -0,0 +1,3 @@
+body {
+    overflow: auto !important;
+}