Browse Source

Bugfix. Determine whether MUC is new when joining...

and wait until MUC features fetched before parsing MUC presence.

Otherwise we have a race condition where don't know whether the MUC
supports occupant IDs and therefore don't store them, resulting in
duplicate occupants.
JC Brand 5 months ago
parent
commit
2839c4671a
29 changed files with 456 additions and 260 deletions
  1. 3 1
      src/headless/plugins/bookmarks/tests/bookmarks.js
  2. 1 1
      src/headless/plugins/disco/api.js
  3. 5 4
      src/headless/plugins/disco/entity.js
  4. 23 17
      src/headless/plugins/muc/muc.js
  5. 2 0
      src/headless/plugins/muc/parsers.js
  6. 8 6
      src/headless/plugins/muc/tests/muc.js
  7. 25 0
      src/headless/plugins/muc/tests/occupants.js
  8. 3 2
      src/headless/plugins/muc/utils.js
  9. 12 4
      src/headless/types/plugins/muc/muc.d.ts
  10. 8 1
      src/headless/types/plugins/muc/utils.d.ts
  11. 6 0
      src/plugins/bookmark-views/tests/bookmarks-list.js
  12. 9 6
      src/plugins/bookmark-views/tests/bookmarks.js
  13. 9 1
      src/plugins/bookmark-views/tests/deprecated.js
  14. 2 1
      src/plugins/muc-views/tests/component.js
  15. 6 4
      src/plugins/muc-views/tests/member-lists.js
  16. 14 7
      src/plugins/muc-views/tests/muc-add-modal.js
  17. 15 3
      src/plugins/muc-views/tests/muc-api.js
  18. 3 3
      src/plugins/muc-views/tests/muc-avatar.js
  19. 4 0
      src/plugins/muc-views/tests/muc-list-modal.js
  20. 3 4
      src/plugins/muc-views/tests/muc-mentions.js
  21. 105 66
      src/plugins/muc-views/tests/muc.js
  22. 102 97
      src/plugins/muc-views/tests/nickname.js
  23. 1 1
      src/plugins/muc-views/tests/rai.js
  24. 17 7
      src/plugins/roomslist/tests/grouplists.js
  25. 27 11
      src/plugins/roomslist/tests/roomslist.js
  26. 30 10
      src/shared/tests/mock.js
  27. 4 0
      src/types/plugins/muc-views/heading.d.ts
  28. 8 2
      src/types/plugins/muc-views/nickname-form.d.ts
  29. 1 1
      src/types/plugins/muc-views/templates/muc-nickname-form.d.ts

+ 3 - 1
src/headless/plugins/bookmarks/tests/bookmarks.js

@@ -213,6 +213,9 @@ describe("A bookmark", function () {
                 nick: ''
             });
             expect(_converse.api.rooms.create).toHaveBeenCalled();
+
+            await mock.getRoomFeatures(_converse, jid);
+            await mock.waitForReservedNick(_converse, jid, '');
             await u.waitUntil(() => state.chatboxes.length === 2);
 
             bookmarks.remove(model);
@@ -291,7 +294,6 @@ describe("A bookmark", function () {
 
         const bare_jid = _converse.session.get('bare_jid');
         const muc1_jid = 'theplay@conference.shakespeare.lit';
-        const { bookmarks } = _converse.state;
         const { api } = _converse;
 
         await api.bookmarks.set({

+ 1 - 1
src/headless/plugins/disco/api.js

@@ -396,7 +396,7 @@ export default {
                 entity.queryInfo();
             } else {
                 // Create it if it doesn't exist
-                entity = await api.disco.entities.create({ jid }, { 'ignore_cache': true });
+                entity = await api.disco.entities.create({ jid }, { ignore_cache: true });
             }
             return entity.waitUntilFeaturesDiscovered;
         },

+ 5 - 4
src/headless/plugins/disco/entity.js

@@ -1,13 +1,14 @@
+import { Collection, Model } from '@converse/skeletor';
+import { getOpenPromise } from '@converse/openpromise';
 import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import converse from '../../shared/api/public.js';
+import { parseErrorStanza } from '../../shared/parsers.js';
 import log from '../../log.js';
 import sizzle from 'sizzle';
-import { Collection, Model } from '@converse/skeletor';
-import { getOpenPromise } from '@converse/openpromise';
 import { createStore } from '../../utils/storage.js';
 
-const { Strophe } = converse.env;
+const { Strophe, u } = converse.env;
 
 /**
  * @class
@@ -126,7 +127,7 @@ class DiscoEntity extends Model {
             stanza = await api.disco.info(this.get('jid'), null);
         } catch (iq) {
             iq === null ? log.error(`Timeout for disco#info query for ${this.get('jid')}`) : log.error(iq);
-            this.waitUntilFeaturesDiscovered.resolve(this);
+            this.waitUntilFeaturesDiscovered.resolve(u.isElement(iq) ? await parseErrorStanza(iq) : iq);
             return;
         }
         this.onInfo(stanza);

+ 23 - 17
src/headless/plugins/muc/muc.js

@@ -23,7 +23,7 @@ import {
 } from './constants.js';
 import { CHATROOMS_TYPE, GONE, INACTIVE, METADATA_ATTRIBUTES } from '../../shared/constants.js';
 import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js';
-import { TimeoutError } from '../../shared/errors.js';
+import { TimeoutError, ItemNotFoundError, StanzaError } from '../../shared/errors.js';
 import { computeAffiliationsDelta, setAffiliations, getAffiliationList } from './affiliations/utils.js';
 import { initStorage, createStore } from '../../utils/storage.js';
 import { isArchived, parseErrorStanza } from '../../shared/parsers.js';
@@ -116,7 +116,7 @@ class MUC extends ModelWithMessages(ColorAwareModel(ChatBoxBase)) {
 
         const restored = await this.restoreFromCache();
         if (!restored) {
-            this.join();
+            await this.join();
         }
         /**
          * Triggered once a {@link MUC} has been created and initialized.
@@ -170,26 +170,28 @@ class MUC extends ModelWithMessages(ColorAwareModel(ChatBoxBase)) {
      * @param {String} [password] - Optional password, if required by the groupchat.
      *  Will fall back to the `password` value stored in the room
      *  model (if available).
+     *  @returns {Promise<void>}
      */
     async join (nick, password) {
         if (this.isEntered()) {
             // We have restored a groupchat from session storage,
             // so we don't send out a presence stanza again.
-            return this;
+            return;
         }
         // Set this early, so we don't rejoin in onHiddenChange
         this.session.save('connection_status', ROOMSTATUS.CONNECTING);
-        await this.refreshDiscoInfo();
+
+        const is_new = (await this.refreshDiscoInfo() instanceof ItemNotFoundError);
         nick = await this.getAndPersistNickname(nick);
         if (!nick) {
             safeSave(this.session, { 'connection_status': ROOMSTATUS.NICKNAME_REQUIRED });
-            if (api.settings.get('muc_show_logs_before_join')) {
+            if (!is_new && api.settings.get('muc_show_logs_before_join')) {
                 await this.fetchMessages();
             }
-            return this;
+            return;
         }
-        api.send(await this.constructJoinPresence(password));
-        return this;
+        api.send(await this.constructJoinPresence(password, is_new));
+        if (is_new) await this.refreshDiscoInfo();
     }
 
     /**
@@ -204,16 +206,19 @@ class MUC extends ModelWithMessages(ColorAwareModel(ChatBoxBase)) {
 
     /**
      * @param {string} password
+     * @param {boolean} is_new
      */
-    async constructJoinPresence (password) {
+    async constructJoinPresence (password, is_new) {
+        const maxstanzas = (is_new || this.features.get('mam_enabled'))
+            ? 0
+            : api.settings.get('muc_history_max_stanzas');
+
         let stanza = $pres({
             'id': getUniqueId(),
             'from': api.connection.get().jid,
             'to': this.getRoomJIDAndNick()
         }).c('x', { 'xmlns': Strophe.NS.MUC })
-          .c('history', {
-                'maxstanzas': this.features.get('mam_enabled') ? 0 : api.settings.get('muc_history_max_stanzas')
-            }).up();
+          .c('history', { maxstanzas }).up();
 
         password = password || this.get('password');
         if (password) {
@@ -1206,11 +1211,12 @@ class MUC extends ModelWithMessages(ColorAwareModel(ChatBoxBase)) {
      * *fields* are stored on the config {@link Model} attribute on this {@link MUC}.
      * @returns {Promise}
      */
-    refreshDiscoInfo () {
-        return api.disco
-            .refresh(this.get('jid'))
-            .then(() => this.getDiscoInfo())
-            .catch((e) => log.error(e));
+    async refreshDiscoInfo () {
+        const result = await api.disco.refresh(this.get('jid'));
+        if (result instanceof StanzaError) {
+            return result;
+        }
+        return this.getDiscoInfo().catch((e) => log.error(e));
     }
 
     /**

+ 2 - 0
src/headless/plugins/muc/parsers.js

@@ -409,6 +409,8 @@ function parsePresenceUserItem(stanza, nick) {
  * @returns {Promise<import('./types').MUCPresenceAttributes>}
  */
 export async function parseMUCPresence(stanza, chatbox) {
+    await chatbox.initialized;
+
     /**
      * @typedef {import('./types').MUCPresenceAttributes} MUCPresenceAttributes
      */

+ 8 - 6
src/headless/plugins/muc/tests/muc.js

@@ -4,6 +4,8 @@ const { Strophe, sizzle, stx, u } = converse.env;
 
 describe("Groupchats", function () {
 
+    beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
+
     it("keeps track of unread messages and mentions",
             mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
 
@@ -51,12 +53,12 @@ describe("Groupchats", function () {
             const muc = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
 
             let pres = await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'presence').pop());
-            expect(Strophe.serialize(pres)).toBe(
-                `<presence from="${_converse.jid}" id="${pres.getAttribute('id')}" to="${muc_jid}/romeo" xmlns="jabber:client">`+
-                    `<x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/></x>`+
-                    `<show>away</show>`+
-                    `<c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>`+
-                `</presence>`);
+            expect(pres).toEqualStanza(stx`
+                <presence from="${_converse.jid}" id="${pres.getAttribute('id')}" to="${muc_jid}/romeo" xmlns="jabber:client">
+                    <x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/></x>
+                    <show>away</show>
+                    <c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>
+                </presence>`);
 
             expect(muc.getOwnOccupant().get('show')).toBe('away');
 

+ 25 - 0
src/headless/plugins/muc/tests/occupants.js

@@ -64,6 +64,31 @@ describe("A MUC occupant", function () {
         expect(model.occupants.length).toBe(mock.chatroom_names.length + 1);
     }));
 
+    it("stores our own XEP-0421 occupant id received from a presence stanza when joining a new MUC",
+            mock.initConverse([], {}, async function (_converse) {
+
+        await mock.waitUntilBookmarksReturned(_converse);
+
+        const { api } = _converse;
+        const muc_jid = 'lounge@montague.lit';
+        const nick = 'romeo';
+        const features = [...mock.default_muc_features, Strophe.NS.OCCUPANTID];
+
+        const promise = api.rooms.open(muc_jid, { nick });
+        await mock.waitForNewMUCDiscoInfo(_converse, muc_jid);
+        await mock.receiveOwnMUCPresence(_converse, muc_jid, nick, 'owner', 'moderator', features);
+        await mock.waitForMUCDiscoInfo(_converse, muc_jid, features);
+
+        const model = await promise;
+
+        const entity = await api.disco.entities.get(muc_jid);
+        expect(entity.getFeature(Strophe.NS.OCCUPANTID)).toBeTruthy();
+        expect(model.occupants.length).toBe(1);
+        expect(model.get('occupant_id')).not.toBeFalsy();
+        expect(model.get('occupant_id')).toBe(model.occupants.at(0).get('occupant_id'));
+    }));
+
+
     it("will be added to a MUC message based on the XEP-0421 occupant id",
             mock.initConverse([], {}, async function (_converse) {
 

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

@@ -102,7 +102,8 @@ export async function routeToRoom (event) {
     api.rooms.open(jid, {}, true);
 }
 
-/* Opens a groupchat, making sure that certain attributes
+/**
+ * Opens a groupchat, making sure that certain attributes
  * are correct, for example that the "type" is set to
  * "chatroom".
  * @param {string} jid
@@ -150,7 +151,7 @@ export async function onDirectMUCInvitation (message) {
     }
 
     if (result) {
-        const chatroom = await openChatRoom(room_jid, { 'password': x_el.getAttribute('password') });
+        const chatroom = await openChatRoom(room_jid, { password: x_el.getAttribute('password') });
         if (chatroom.session.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED) {
             _converse.state.chatboxes.get(room_jid).rejoin();
         }

+ 12 - 4
src/headless/types/plugins/muc/muc.d.ts

@@ -237,7 +237,7 @@ declare class MUC extends MUC_base {
      */
     public vcard: import("../vcard/vcard").default;
     initialized: any;
-    debouncedRejoin: import("lodash").DebouncedFunc<() => Promise<this>>;
+    debouncedRejoin: import("lodash").DebouncedFunc<() => Promise<void>>;
     isEntered(): boolean;
     /**
      * Checks whether this MUC qualifies for subscribing to XEP-0437 Room Activity Indicators (RAI)
@@ -255,16 +255,18 @@ declare class MUC extends MUC_base {
      * @param {String} [password] - Optional password, if required by the groupchat.
      *  Will fall back to the `password` value stored in the room
      *  model (if available).
+     *  @returns {Promise<void>}
      */
-    join(nick?: string, password?: string): Promise<this>;
+    join(nick?: string, password?: string): Promise<void>;
     /**
      * Clear stale cache and re-join a MUC we've been in before.
      */
-    rejoin(): Promise<this>;
+    rejoin(): Promise<void>;
     /**
      * @param {string} password
+     * @param {boolean} is_new
      */
-    constructJoinPresence(password: string): Promise<import("strophe.js").Builder>;
+    constructJoinPresence(password: string, is_new: boolean): Promise<import("strophe.js").Builder>;
     clearOccupantsCache(): void;
     /**
      * Given the passed in MUC message, send a XEP-0333 chat marker.
@@ -586,6 +588,12 @@ declare class MUC extends MUC_base {
      *  to update the list.
      */
     updateMemberLists(members: object): Promise<any>;
+    /**
+     * Triggers a hook which gives 3rd party plugins an opportunity to determine
+     * the nickname to use.
+     * @return {Promise<string>} A promise which resolves with the nickname
+     */
+    getNicknameFromHook(): Promise<string>;
     /**
      * Given a nick name, save it to the model state, otherwise, look
      * for a server-side reserved nickname or default configured

+ 8 - 1
src/headless/types/plugins/muc/utils.d.ts

@@ -19,7 +19,14 @@ export function onWindowStateChanged(): Promise<void>;
  * @param {Event} [event]
  */
 export function routeToRoom(event?: Event): Promise<void>;
-export function openChatRoom(jid: any, settings: any): Promise<any>;
+/**
+ * Opens a groupchat, making sure that certain attributes
+ * are correct, for example that the "type" is set to
+ * "chatroom".
+ * @param {string} jid
+ * @param {Object} settings
+ */
+export function openChatRoom(jid: string, settings: any): Promise<any>;
 /**
  * A direct MUC invitation to join a groupchat has been received
  * See XEP-0249: Direct MUC invitations.

+ 6 - 0
src/plugins/bookmark-views/tests/bookmarks-list.js

@@ -126,11 +126,17 @@ describe("The bookmarks list modal", function () {
         await u.waitUntil(() => modal.querySelectorAll('.bookmarks.rooms-list .room-item').length);
         expect(modal.querySelectorAll('.bookmarks.rooms-list .room-item').length).toBe(2);
         modal.querySelector('.bookmarks.rooms-list .open-room').click();
+
+        await mock.getRoomFeatures(_converse, 'first@conference.shakespeare.lit');
+        await mock.waitForReservedNick(_converse, 'first@conference.shakespeare.lit', '');
         await u.waitUntil(() => _converse.chatboxes.length === 2);
         expect((await api.rooms.get('first@conference.shakespeare.lit')).get('hidden')).toBe(false);
 
         await u.waitUntil(() => modal.querySelectorAll('.list-container--bookmarks .available-chatroom').length);
         modal.querySelector('.list-container--bookmarks .available-chatroom:last-child .open-room').click();
+
+        await mock.getRoomFeatures(_converse, 'theplay@conference.shakespeare.lit');
+        await mock.waitForReservedNick(_converse, 'theplay@conference.shakespeare.lit', '');
         await u.waitUntil(() => _converse.chatboxes.length === 3);
 
         expect((await api.rooms.get('first@conference.shakespeare.lit')).get('hidden')).toBe(true);

+ 9 - 6
src/plugins/bookmark-views/tests/bookmarks.js

@@ -20,10 +20,10 @@ describe("A chat room", function () {
                 'nick': 'Othello'
             });
             spyOn(_converse.ChatRoom.prototype, 'getAndPersistNickname').and.callThrough();
-            const room_creation_promise = _converse.api.rooms.open(muc_jid);
+            _converse.api.rooms.open(muc_jid);
             await mock.getRoomFeatures(_converse, muc_jid);
-            const room = await room_creation_promise;
-            await u.waitUntil(() => room.getAndPersistNickname.calls.count());
+            await mock.waitForReservedNick(_converse, muc_jid);
+            const room = await u.waitUntil(() => _converse.chatboxes.get(muc_jid));
             expect(room.get('nick')).toBe('Othello');
         }));
     });
@@ -36,6 +36,7 @@ describe("Bookmarks", function () {
     it("can be pushed from the XMPP server", mock.initConverse(
             ['connected', 'chatBoxesFetched'], {}, async function (_converse) {
 
+        const { api } = _converse;
         const { u } = converse.env;
         await mock.waitForRoster(_converse, 'current', 0);
         await mock.waitUntilBookmarksReturned(_converse);
@@ -67,12 +68,13 @@ describe("Bookmarks", function () {
             </event>
         </message>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
+        await mock.getRoomFeatures(_converse, 'theplay@conference.shakespeare.lit');
 
         const { bookmarks } = _converse.state;
         await u.waitUntil(() => bookmarks.length);
         expect(bookmarks.length).toBe(2);
         expect(bookmarks.map(b => b.get('name'))).toEqual(['Another bookmark', "The Play's the Thing"]);
-        expect(_converse.chatboxviews.get('theplay@conference.shakespeare.lit')).not.toBeUndefined();
+        expect(await api.rooms.get('theplay@conference.shakespeare.lit')).not.toBeUndefined();
 
         stanza = stx`<message from="romeo@montague.lit"
                         to="${_converse.jid}"
@@ -247,10 +249,11 @@ describe("Bookmarks", function () {
         expect(theplay.get('name')).toBe("The Play's the Thing");
         expect(theplay.get('nick')).toBe('JC');
         expect(theplay.get('password')).toBe('secret');
-
         expect(bookmarks.get('orchard@conference.shakespeare.lit').get('autojoin')).toBe(false);
 
+        await mock.getRoomFeatures(_converse, autojoin_muc);
         await u.waitUntil(() => _converse.state.chatboxes.get(autojoin_muc));
+
         const features = [
             'http://jabber.org/protocol/muc',
             'jabber:iq:register',
@@ -269,7 +272,7 @@ describe("Bookmarks", function () {
                 id="${sent_stanza.getAttribute('id')}"
                 to="${autojoin_muc}/JC">
             <x xmlns="http://jabber.org/protocol/muc">
-                <history/>
+                <history maxstanzas="0"/>
                 <password>secret</password>
             </x>
             <c xmlns="http://jabber.org/protocol/caps"

+ 9 - 1
src/plugins/bookmark-views/tests/deprecated.js

@@ -8,6 +8,7 @@ describe("Bookmarks", function () {
     it("can be pushed from the XMPP server", mock.initConverse(
             ['connected', 'chatBoxesFetched'], {}, async function (_converse) {
 
+        const { api } = _converse;
         const { u } = converse.env;
         await mock.waitForRoster(_converse, 'current', 0);
         await mock.waitUntilBookmarksReturned(
@@ -37,12 +38,13 @@ describe("Bookmarks", function () {
             </event>
         </message>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
+        await mock.getRoomFeatures(_converse, 'theplay@conference.shakespeare.lit');
 
         const { bookmarks } = _converse.state;
         await u.waitUntil(() => bookmarks.length);
         expect(bookmarks.length).toBe(2);
         expect(bookmarks.map(b => b.get('name'))).toEqual(['Another bookmark', "The Play's the Thing"]);
-        expect(_converse.chatboxviews.get('theplay@conference.shakespeare.lit')).not.toBeUndefined();
+        expect(await api.rooms.get('theplay@conference.shakespeare.lit')).not.toBeUndefined();
 
         stanza = stx`<message from='romeo@montague.lit' to='${_converse.jid}' type='headline' id='${u.getUniqueId()}' xmlns="jabber:client">
             <event xmlns='http://jabber.org/protocol/pubsub#event'>
@@ -251,11 +253,17 @@ describe("The bookmarks list modal", function () {
         await u.waitUntil(() => modal.querySelectorAll('.bookmarks.rooms-list .room-item').length);
         expect(modal.querySelectorAll('.bookmarks.rooms-list .room-item').length).toBe(2);
         modal.querySelector('.bookmarks.rooms-list .open-room').click();
+
+        await mock.getRoomFeatures(_converse, 'first@conference.shakespeare.lit');
+        await mock.waitForReservedNick(_converse, 'first@conference.shakespeare.lit', '');
         await u.waitUntil(() => _converse.chatboxes.length === 2);
         expect((await api.rooms.get('first@conference.shakespeare.lit')).get('hidden')).toBe(false);
 
         await u.waitUntil(() => modal.querySelectorAll('.list-container--bookmarks .available-chatroom').length);
         modal.querySelector('.list-container--bookmarks .available-chatroom:last-child .open-room').click();
+
+        await mock.getRoomFeatures(_converse, 'theplay@conference.shakespeare.lit');
+        await mock.waitForReservedNick(_converse, 'theplay@conference.shakespeare.lit', '');
         await u.waitUntil(() => _converse.chatboxes.length === 3);
 
         expect((await api.rooms.get('first@conference.shakespeare.lit')).get('hidden')).toBe(true);

+ 2 - 1
src/plugins/muc-views/tests/component.js

@@ -11,9 +11,10 @@ describe("The <converse-muc> component", function () {
         const { api } = _converse;
         const muc_jid = 'lounge@montague.lit';
         const nick = 'romeo';
-        const muc_creation_promise = await api.rooms.open(muc_jid, {nick, 'hidden': true}, false);
+        const muc_creation_promise = api.rooms.open(muc_jid, {nick, 'hidden': true}, false);
         await mock.getRoomFeatures(_converse, muc_jid, []);
         await mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
+
         await muc_creation_promise;
         const model = _converse.chatboxes.get(muc_jid);
         await u.waitUntil(() => (model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED));

+ 6 - 4
src/plugins/muc-views/tests/member-lists.js

@@ -92,11 +92,12 @@ describe("A Groupchat", function () {
                     Strophe.NS.SID
                 ];
                 const nick = 'romeo';
-                await _converse.api.rooms.open(muc_jid);
+                _converse.api.rooms.open(muc_jid);
                 await mock.getRoomFeatures(_converse, muc_jid, features);
                 await mock.waitForReservedNick(_converse, muc_jid, nick);
                 mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
-                const view = _converse.chatboxviews.get(muc_jid);
+
+                const view = await u.waitUntil(() => _converse.chatboxviews.get(muc_jid));
                 await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED));
 
                 // Check in reverse order that we requested all three lists
@@ -173,8 +174,6 @@ describe("Someone being invited to a groupchat", function () {
                 `<query xmlns="http://jabber.org/protocol/disco#info"/>`+
             `</iq>`);
 
-        // State that the chat is members-only via the features IQ
-        const view = _converse.chatboxviews.get(muc_jid);
         const features_stanza = stx`
             <iq from="coven@chat.shakespeare.lit"
                 id="${stanza.getAttribute('id')}"
@@ -192,6 +191,9 @@ describe("Someone being invited to a groupchat", function () {
         _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
         const sent_stanzas = _converse.api.connection.get().sent_stanzas;
         await u.waitUntil(() => sent_stanzas.filter(s => s.matches(`presence[to="${muc_jid}/${nick}"]`)).pop());
+
+        // State that the chat is members-only via the features IQ
+        const view = await u.waitUntil(() => _converse.chatboxviews.get(muc_jid));
         expect(view.model.features.get('membersonly')).toBeTruthy();
 
         await room_creation_promise;

+ 14 - 7
src/plugins/muc-views/tests/muc-add-modal.js

@@ -9,7 +9,10 @@ describe('The "Groupchats" Add modal', function () {
         mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
             const modal = await mock.openAddMUCModal(_converse);
 
+            const muc_jid = 'lounge@muc.montague.lit';
+
             let label_name = modal.querySelector('label[for="chatroom"]');
+            expect(modal.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat');
             expect(label_name.textContent.trim()).toBe('Groupchat name or address:');
             const label_nick = modal.querySelector('label[for="nickname"]');
             expect(label_nick.textContent.trim()).toBe('Nickname:');
@@ -17,19 +20,19 @@ describe('The "Groupchats" Add modal', function () {
             expect(nick_input.value).toBe('Romeo');
             nick_input.value = 'romeo';
 
-            expect(modal.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat');
-            spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
-            modal.querySelector('input[name="chatroom"]').value = 'lounge@muc.montague.lit';
+            modal.querySelector('input[name="chatroom"]').value = muc_jid;
             modal.querySelector('form input[type="submit"]').click();
+
+            await mock.getRoomFeatures(_converse, muc_jid);
             await u.waitUntil(() => _converse.chatboxes.length);
             await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1);
         })
     );
 
     it("doesn't require the domain when muc_domain is set",
-        mock.initConverse(['chatBoxesFetched'], { 'muc_domain': 'muc.example.org' }, async function (_converse) {
-            const modal = await mock.openAddMUCModal(_converse);
+        mock.initConverse(['chatBoxesFetched'], { muc_domain: 'muc.example.org' }, async function (_converse) {
 
+            const modal = await mock.openAddMUCModal(_converse);
             expect(modal.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat');
             spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
             const label_name = modal.querySelector('label[for="chatroom"]');
@@ -40,6 +43,7 @@ describe('The "Groupchats" Add modal', function () {
             nick_input.value = 'max';
 
             modal.querySelector('form input[type="submit"]').click();
+            await mock.getRoomFeatures(_converse, 'lounge@muc.example.org');
             await u.waitUntil(() => _converse.chatboxes.length);
             await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1);
             expect(_converse.chatboxes.models.map(m => m.get('id')).includes('lounge@muc.example.org')).toBe(true);
@@ -53,6 +57,7 @@ describe('The "Groupchats" Add modal', function () {
             nick_input = modal.querySelector('input[name="nickname"]');
             nick_input.value = 'max';
             modal.querySelector('form input[type="submit"]').click();
+            await mock.getRoomFeatures(_converse, 'lounge@conference.example.org');
             await u.waitUntil(() => _converse.chatboxes.models.filter(c => c.get('type') === 'chatroom').length === 2);
             await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 2);
             expect(_converse.chatboxes.models.map(m => m.get('id')).includes('lounge@conference.example.org')).toBe(
@@ -76,6 +81,7 @@ describe('The "Groupchats" Add modal', function () {
             let nick_input = modal.querySelector('input[name="nickname"]');
             nick_input.value = 'max';
             modal.querySelector('form input[type="submit"]').click();
+            await mock.getRoomFeatures(_converse, 'lounge@muc.example.org');
             await u.waitUntil(() => _converse.chatboxes.length);
             await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1);
             expect(_converse.chatboxes.models.map(m => m.get('id')).includes('lounge@muc.example.org')).toBe(true);
@@ -89,6 +95,7 @@ describe('The "Groupchats" Add modal', function () {
             nick_input = modal.querySelector('input[name="nickname"]');
             nick_input.value = 'max';
             modal.querySelector('form input[type="submit"]').click();
+            await mock.getRoomFeatures(_converse, 'lounge-conference@muc.example.org');
             await u.waitUntil(
                 () => _converse.chatboxes.models.filter(c => c.get('type') === 'chatroom').length === 2
             );
@@ -126,7 +133,7 @@ describe('The "Groupchats" Add modal', function () {
 
             await mock.waitUntilDiscoConfirmed(_converse, domain, [], [], ['muc.example.org'], 'items');
             await mock.waitUntilDiscoConfirmed(_converse, 'muc.example.org', [], [Strophe.NS.MUC]);
-
+            await mock.getRoomFeatures(_converse, muc_jid);
 
             await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1);
             expect(_converse.chatboxes.models.map(m => m.get('id')).includes(muc_jid)).toBe(true);
@@ -249,6 +256,7 @@ describe('The "Groupchats" Add modal', function () {
 
             modal.querySelector('form input[type="submit"]').click();
 
+            await mock.getRoomFeatures(_converse, 'into-the-ather-a-journey@montague.lit');
             await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1);
             expect(_converse.chatboxes.models.map(m => m.get('id')).includes('into-the-ather-a-journey@montague.lit')).toBe(true);
 
@@ -273,7 +281,6 @@ describe('The "Groupchats" Add modal', function () {
             nick_input.value = 'max';
 
             modal.querySelector('form input[type="submit"]').click();
-
             await u.waitUntil(() => name_input.classList.contains('error'));
             expect(name_input.classList.contains('is-invalid')).toBe(true);
             expect(modal.querySelector('.invalid-feedback')?.textContent).toBe('Groupchat id is invalid.');

+ 15 - 3
src/plugins/muc-views/tests/muc-api.js

@@ -110,13 +110,18 @@ describe("Groupchats", function () {
             spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
 
             let jid = 'lounge@montague.lit';
+            const nick = 'romeo';
             await mock.openControlBox(_converse);
             await mock.waitForRoster(_converse, 'current');
             const rosterview = document.querySelector('converse-roster');
             await u.waitUntil(() => rosterview.querySelectorAll('.roster-group .group-toggle').length);
 
-            let room = await _converse.api.rooms.open(jid);
             // Test on groupchat that's not yet open
+            let promise = _converse.api.rooms.open(jid);
+            await mock.getRoomFeatures(_converse, jid);
+            await mock.waitForReservedNick(_converse, jid, nick);
+
+            let room = await promise;
             expect(room instanceof Model).toBeTruthy();
             let mucview = await u.waitUntil(() => _converse.chatboxviews.get(jid));
             expect(mucview.is_chatroom).toBeTruthy();
@@ -132,7 +137,10 @@ describe("Groupchats", function () {
 
             // Test with mixed case in JID
             jid = 'Leisure@montague.lit';
-            room = await _converse.api.rooms.open(jid);
+            promise  = _converse.api.rooms.open(jid);
+            await mock.getRoomFeatures(_converse, jid);
+            await mock.waitForReservedNick(_converse, jid, nick);
+            room = await promise;
             expect(room instanceof Model).toBeTruthy();
             mucview = await u.waitUntil(() => _converse.chatboxviews.get(jid.toLowerCase()));
             await u.waitUntil(() => u.isVisible(mucview));
@@ -151,8 +159,10 @@ describe("Groupchats", function () {
             mucview.close();
 
             api.settings.set('muc_instant_rooms', false);
+
             // Test with configuration
-            room = await _converse.api.rooms.open('room@conference.example.org', {
+            jid = 'room@conference.example.org';
+            promise = _converse.api.rooms.open(jid, {
                 'nick': 'some1',
                 'auto_configure': true,
                 'roomconfig': {
@@ -165,6 +175,8 @@ describe("Groupchats", function () {
                     'whois': 'anyone'
                 }
             });
+            await mock.getRoomFeatures(_converse, jid);
+            room = await promise;
             expect(room instanceof Model).toBeTruthy();
 
             const IQ_stanzas = _converse.api.connection.get().IQ_stanzas;

+ 3 - 3
src/plugins/muc-views/tests/muc-avatar.js

@@ -44,7 +44,7 @@ describe('Groupchats', () => {
                     const muc_jid = 'coven@chat.shakespeare.lit';
                     await mock.waitForRoster(_converse, 'current', 0);
                     await mock.openControlBox(_converse);
-                    await _converse.api.rooms.open(muc_jid, { 'nick': 'some1' });
+                    const promise = _converse.api.rooms.open(muc_jid, { nick: 'some1' });
 
                     const selector = `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`;
                     const features_query = await u.waitUntil(() =>
@@ -82,7 +82,7 @@ describe('Groupchats', () => {
                         </iq>`;
                     _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
 
-                    const view = _converse.chatboxviews.get(muc_jid);
+                    const view = await u.waitUntil(() => _converse.chatboxviews.get(muc_jid));
                     await u.waitUntil(
                         () => view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING
                     );
@@ -104,7 +104,7 @@ describe('Groupchats', () => {
 
                     const initials_el = avatar_el.querySelector('.avatar-initials');
                     expect(initials_el.textContent).toBe('AC');
-                    expect(getComputedStyle(initials_el).backgroundColor).toBe('rgb(75, 103, 255)');
+                    await u.waitUntil(() => getComputedStyle(initials_el).backgroundColor === 'rgb(75, 103, 255)');
                     avatar_el.click();
 
                     const modal = _converse.api.modal.get('converse-muc-details-modal');

+ 4 - 0
src/plugins/muc-views/tests/muc-list-modal.js

@@ -73,7 +73,11 @@ describe('The "Groupchats" List modal', function () {
             expect(rooms[10].textContent.trim()).toBe('A street');
 
             rooms[4].querySelector('.open-room').click();
+
+            await mock.getRoomFeatures(_converse, 'inverness@chat.shakespeare.lit');
+            await mock.waitForReservedNick(_converse, 'inverness@chat.shakespeare.lit', 'romeo');
             await u.waitUntil(() => _converse.chatboxes.length > 1);
+
             expect(sizzle('.chatroom', _converse.el).filter(u.isVisible).length).toBe(1); // There should now be an open chatroom
             const view = _converse.chatboxviews.get('inverness@chat.shakespeare.lit');
             expect(view.querySelector('.chatbox-title__text').textContent.trim()).toBe("Macbeth's Castle");

+ 3 - 4
src/plugins/muc-views/tests/muc-mentions.js

@@ -13,13 +13,12 @@ describe("MUC Mention Notfications", function () {
                 view_mode: 'overlayed'},
             async function (_converse) {
 
-        const { api } = _converse;
-
         expect(_converse.session.get('rai_enabled_domains')).toBe(undefined);
 
-        const muc_jid = 'lounge@montague.lit';
+        const { api } = _converse;
         const nick = 'romeo';
-        const muc_creation_promise = await api.rooms.open(muc_jid, { nick }, false);
+        const muc_jid = 'lounge@montague.lit';
+        const muc_creation_promise = api.rooms.open(muc_jid, { nick }, false);
         await mock.getRoomFeatures(_converse, muc_jid, []);
         await mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
         await muc_creation_promise;

+ 105 - 66
src/plugins/muc-views/tests/muc.js

@@ -4,22 +4,39 @@ const { $pres, Strophe, Promise, sizzle, stx, u }  = converse.env;
 
 describe("Groupchats", function () {
 
+    beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
+
     describe("An instant groupchat", function () {
 
         it("will be created when muc_instant_rooms is set to true",
                 mock.initConverse(['chatBoxesFetched'], { vcard: { nickname: '' } }, async function (_converse) {
 
             let IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
+
+            const { api } = _converse;
             const muc_jid = 'lounge@montague.lit';
             const nick = 'nicky';
-            await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
+            const promise = api.rooms.open(muc_jid);
+            await mock.getRoomFeatures(_converse, muc_jid);
+            await mock.waitForReservedNick(_converse, muc_jid, '');
+            const muc = await promise;
+            await muc.initialized;
+            spyOn(muc, 'join').and.callThrough();
+
+            const view = _converse.chatboxviews.get(muc_jid);
+            const input = await u.waitUntil(() => view.querySelector('input[name="nick"]'), 1000);
+            expect(view.model.session.get('connection_status')).toBe(converse.ROOMSTATUS.NICKNAME_REQUIRED);
+
+            input.value = nick;
+            view.querySelector('input[type=submit]').click();
+            expect(view.model.join).toHaveBeenCalled();
 
             const disco_selector = `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`;
             const stanza = await u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector(disco_selector)).pop());
 
             // We pretend this is a new room, so no disco info is returned.
             const features_stanza =
-                stx`<iq from="lounge@montague.lit"
+                stx`<iq from="${muc_jid}"
                         id="${stanza.getAttribute('id')}"
                         to="romeo@montague.lit/desktop"
                         type="error"
@@ -30,19 +47,8 @@ describe("Groupchats", function () {
                 </iq>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
 
-            const view = _converse.chatboxviews.get(muc_jid);
-            spyOn(view.model, 'join').and.callThrough();
-            await mock.waitForReservedNick(_converse, muc_jid, '');
-            const input = await u.waitUntil(() => view.querySelector('input[name="nick"]'), 1000);
-            expect(view.model.session.get('connection_status')).toBe(converse.ROOMSTATUS.NICKNAME_REQUIRED);
-            input.value = nick;
-            view.querySelector('input[type=submit]').click();
-            expect(view.model.join).toHaveBeenCalled();
-
             _converse.api.connection.get().IQ_stanzas = [];
-            await mock.getRoomFeatures(_converse, muc_jid);
             await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING);
-            await mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
 
             // The user has just entered the room (because join was called)
             // and receives their own presence from the server.
@@ -97,8 +103,15 @@ describe("Groupchats", function () {
                     async function (_converse) {
 
             const { api } = _converse;
+            const muc_jid = 'orchard@chat.shakespeare.lit';
+            api.rooms.get(muc_jid);
+            await mock.getRoomFeatures(_converse, muc_jid);
+            await mock.waitForReservedNick(_converse, muc_jid, 'romeo');
+            await mock.receiveOwnMUCPresence(_converse, muc_jid, 'romeo');
+
             await api.waitUntil('roomsAutoJoined');
-            const room = await api.rooms.get('orchard@chat.shakespeare.lit');
+
+            const room = await u.waitUntil(() => _converse.chatboxes.get(muc_jid));
             expect(room.get('hidden')).toBe(false);
         }));
 
@@ -118,7 +131,8 @@ describe("Groupchats", function () {
             api.rooms.open(muc_jid);
             await mock.getRoomFeatures(_converse, muc_jid);
             await mock.waitForReservedNick(_converse, muc_jid);
-            const view = _converse.chatboxviews.get(muc_jid);
+
+            const view = await u.waitUntil(() => _converse.chatboxviews.get(muc_jid));
             await view.model.messages.fetched;
 
             view.model.messages.create({
@@ -510,6 +524,9 @@ describe("Groupchats", function () {
             await view.model.handleMessageStanza(msg);
             await u.waitUntil(()  => view.querySelector('.chat-msg__text a'));
             view.querySelector('.chat-msg__text a').click();
+
+            await mock.getRoomFeatures(_converse, 'coven@chat.shakespeare.lit');
+            await mock.waitForReservedNick(_converse, 'coven@chat.shakespeare.lit', 'romeo');
             await u.waitUntil(() => _converse.chatboxes.length === 3)
             expect(_converse.chatboxes.pluck('id').includes('coven@chat.shakespeare.lit')).toBe(true);
         }));
@@ -519,11 +536,11 @@ describe("Groupchats", function () {
 
             const muc_jid = 'coven@chat.shakespeare.lit';
             const nick = 'romeo';
-            await _converse.api.rooms.open(muc_jid);
+            _converse.api.rooms.open(muc_jid);
             await mock.getRoomFeatures(_converse, muc_jid);
             await mock.waitForReservedNick(_converse, muc_jid, nick);
 
-            const view = _converse.chatboxviews.get(muc_jid);
+            const view = await u.waitUntil(() => _converse.chatboxviews.get(muc_jid));
             const presence =
                 stx`<presence to="romeo@montague.lit/orchard"
                         from="coven@chat.shakespeare.lit/some1"
@@ -553,12 +570,11 @@ describe("Groupchats", function () {
 
             const muc_jid = 'coven@chat.shakespeare.lit';
             const nick = 'some1';
-            const room_creation_promise = await _converse.api.rooms.open(muc_jid, {nick});
+            const room_creation_promise = _converse.api.rooms.open(muc_jid, {nick});
             await mock.getRoomFeatures(_converse, muc_jid);
             const sent_stanzas = _converse.api.connection.get().sent_stanzas;
             await u.waitUntil(() => sent_stanzas.filter(iq => sizzle('presence history', iq).length));
 
-            const view = _converse.chatboxviews.get('coven@chat.shakespeare.lit');
             await _converse.api.waitUntil('chatRoomViewInitialized');
 
             /* We don't show join/leave messages for existing occupants. We
@@ -596,6 +612,7 @@ describe("Groupchats", function () {
                 .c('status', {code: '110'});
             _converse.api.connection.get()._dataRecv(mock.createRequest(presence));
 
+            const view = await u.waitUntil(() => _converse.chatboxviews.get('coven@chat.shakespeare.lit'));
             const csntext = await u.waitUntil(() => view.querySelector('.chat-content__notifications')?.textContent);
             expect(csntext.trim()).toEqual("some1 has entered the groupchat");
 
@@ -1055,26 +1072,15 @@ describe("Groupchats", function () {
                 IQ_id = sendIQ.call(this, iq, callback, errback);
             });
 
+            const { api } = _converse;
+            const own_jid = api.connection.get().jid;
             const muc_jid = 'coven@chat.shakespeare.lit';
-            await _converse.api.rooms.open(muc_jid, {'nick': 'some1'});
-            const view = await u.waitUntil(() => _converse.chatboxviews.get('coven@chat.shakespeare.lit'));
-            await u.waitUntil(() => u.isVisible(view));
+            _converse.api.rooms.open(muc_jid, {'nick': 'some1'});
 
-            // We pretend this is a new room, so no disco info is returned.
-            const features_stanza =
-                stx`<iq from="coven@chat.shakespeare.lit"
-                        id="${IQ_id}"
-                        to="romeo@montague.lit/desktop"
-                        type="error"
-                        xmlns="jabber:client">
-                    <error type="cancel">
-                        <item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
-                    </error>
-                </iq>`;
-            _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
+            await mock.waitOnDiscoInfoForNewMUC(_converse, muc_jid);
 
             const presence =
-                stx`<presence to='romeo@montague.lit/_converse.js-29092160'
+                stx`<presence to='${own_jid}'
                         from='coven@chat.shakespeare.lit/some1'
                         xmlns="jabber:client">
                     <x xmlns='${Strophe.NS.MUC_USER}'>
@@ -1085,9 +1091,15 @@ describe("Groupchats", function () {
                 </presence>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(presence));
 
+            const sent_IQs = _converse.api.connection.get().IQ_stanzas;
+            while (sent_IQs.length) sent_IQs.pop();
+
+            await mock.getRoomFeatures(_converse, muc_jid);
+
+            const view = await u.waitUntil(() => _converse.chatboxviews.get(muc_jid));
+            await u.waitUntil(() => u.isVisible(view));
             await u.waitUntil(() => view.model.getOwnOccupant()?.get('affiliation') === 'owner');
 
-            const sent_IQs = _converse.api.connection.get().IQ_stanzas;
             const sel = 'iq query[xmlns="http://jabber.org/protocol/muc#owner"]';
             const iq = await u.waitUntil(() => sent_IQs.filter(iq => sizzle(sel, iq).length).pop());
 
@@ -1491,7 +1503,11 @@ describe("Groupchats", function () {
         it("properly handles notification that a room has been destroyed",
                 mock.initConverse([], {}, async function (_converse) {
 
-            await mock.openChatRoomViaModal(_converse, 'problematic@muc.montague.lit', 'romeo')
+            const { api } = _converse;
+            const muc_jid = 'problematic@muc.montague.lit';
+            api.rooms.open(muc_jid, { nick: 'romeo' });
+            await mock.getRoomFeatures(_converse, muc_jid);
+
             const presence =
                 stx`<presence from="problematic@muc.montague.lit"
                         id="n13mt3l"
@@ -1504,7 +1520,7 @@ describe("Groupchats", function () {
                     </error>
                 </presence>`;
 
-            const view = _converse.chatboxviews.get('problematic@muc.montague.lit');
+            const view = await u.waitUntil(() => _converse.chatboxviews.get('problematic@muc.montague.lit'));
             _converse.api.connection.get()._dataRecv(mock.createRequest(presence));
             const msg = await u.waitUntil(() => view.querySelector('.chatroom-body .disconnect-msg'));
             expect(msg.textContent.trim()).toBe('This groupchat no longer exists');
@@ -1591,25 +1607,20 @@ describe("Groupchats", function () {
             );
         }));
 
-        it("can be joined automatically, based upon a received invite",
+        it("can be joined automatically, based on a received invite",
                 mock.initConverse([], {}, async function (_converse) {
 
             await mock.waitForRoster(_converse, 'current'); // We need roster contacts, who can invite us
+            const muc_jid = 'lounge@montague.lit';
             const name = mock.cur_names[0];
             const from_jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
             await u.waitUntil(() => _converse.roster.get(from_jid).vcard.get('fullname'));
 
             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.
-
-            const muc_jid = 'lounge@montague.lit';
-            const reason = "Please join this groupchat";
-
             expect(_converse.chatboxes.models.length).toBe(1);
             expect(_converse.chatboxes.models[0].id).toBe("controlbox");
 
+            const reason = "Please join this groupchat";
             const stanza = stx`
                 <message xmlns="jabber:client"
                         to="${_converse.bare_jid}"
@@ -1617,7 +1628,11 @@ describe("Groupchats", function () {
                         id="9bceb415-f34b-4fa4-80d5-c0d076a24231">
                    <x xmlns="jabber:x:conference" jid="${muc_jid}" reason="${reason}"/>
                 </message>`.tree();
-            await _converse.onDirectMUCInvitation(stanza);
+            const promise = _converse.onDirectMUCInvitation(stanza);
+            await mock.getRoomFeatures(_converse, muc_jid);
+            await mock.waitForReservedNick(_converse, muc_jid, 'romeo');
+            await mock.receiveOwnMUCPresence(_converse, muc_jid, 'romeo');
+            await promise;
 
             expect(_converse.api.confirm).toHaveBeenCalledWith(
                 name + ' has invited you to join a groupchat: '+ muc_jid +
@@ -1774,7 +1789,7 @@ describe("Groupchats", function () {
             const IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
             const muc_jid = 'coven@chat.shakespeare.lit';
 
-            await _converse.api.rooms.open(muc_jid, { nick });
+            _converse.api.rooms.open(muc_jid, { nick });
             const stanza = await u.waitUntil(() => IQ_stanzas.filter(
                 iq => iq.querySelector(
                     `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
@@ -2144,7 +2159,10 @@ describe("Groupchats", function () {
         it("can be saved to, and retrieved from, browserStorage",
                 mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
 
-            await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
+            const { api } = _converse;
+            const muc_jid = 'lounge@montague.lit';
+            await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
+
             // We instantiate a new ChatBoxes collection, which by default
             // will be empty.
             await mock.openControlBox(_converse);
@@ -2170,7 +2188,8 @@ describe("Groupchats", function () {
         it("can be closed again by clicking a DOM element with class 'close-chatbox-button'",
                 mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
 
-            const model = await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
+            const muc_jid = 'lounge@montague.lit';
+            const model = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
             spyOn(model, 'close').and.callThrough();
             spyOn(_converse.api, "trigger").and.callThrough();
             spyOn(model, 'leave');
@@ -2287,9 +2306,10 @@ describe("Groupchats", function () {
         it("will show an error message if the groupchat requires a password",
                 mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
 
+            const { api } = _converse;
             const muc_jid = 'protected@montague.lit';
-            await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
-            const view = _converse.chatboxviews.get(muc_jid);
+            api.rooms.open(muc_jid, { nick: 'romeo' });
+            await mock.getRoomFeatures(_converse, muc_jid);
 
             const presence =
                     stx`<presence from="${muc_jid}/romeo"
@@ -2302,9 +2322,9 @@ describe("Groupchats", function () {
                             <not-authorized xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
                         </error>
                     </presence>`;
-
             _converse.api.connection.get()._dataRecv(mock.createRequest(presence));
 
+            const view = await u.waitUntil(() => _converse.chatboxviews.get(muc_jid));
             const chat_body = view.querySelector('.chatroom-body');
             await u.waitUntil(() => chat_body.querySelectorAll('form.chatroom-form').length === 1);
             expect(chat_body.querySelector('.chatroom-form label').textContent.trim())
@@ -2321,14 +2341,20 @@ describe("Groupchats", function () {
         it("will show an error message if the groupchat is members-only and the user not included",
                 mock.initConverse([], {}, async function (_converse) {
 
+            const { api } = _converse;
             const muc_jid = 'members-only@muc.montague.lit'
-            await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
-            const view = _converse.chatboxviews.get(muc_jid);
+            api.rooms.open(muc_jid, { nick: 'romeo' });
+
             const iq = await u.waitUntil(() => _converse.api.connection.get().IQ_stanzas.filter(
                 iq => iq.querySelector(
                     `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
                 )).pop());
 
+            expect(iq).toEqualStanza(stx`
+                <iq from="romeo@montague.lit/orchard" to="${muc_jid}" type="get" xmlns="jabber:client" id="${iq.getAttribute('id')}">
+                    <query xmlns="http://jabber.org/protocol/disco#info"/>
+                </iq>`);
+
             // State that the chat is members-only via the features IQ
             const features_stanza =
                 stx`<iq from="${muc_jid}"
@@ -2345,6 +2371,8 @@ describe("Groupchats", function () {
                     </query>
                 </iq>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
+
+            const view = await u.waitUntil(() => _converse.chatboxviews.get(muc_jid));
             await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING);
 
             const presence =
@@ -2367,8 +2395,9 @@ describe("Groupchats", function () {
         it("will show an error message if the user has been banned",
                 mock.initConverse([], {}, async function (_converse) {
 
+            const { api } = _converse;
             const muc_jid = 'off-limits@muc.montague.lit'
-            await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
+            api.rooms.open(muc_jid, { nick: 'romeo' });
 
             const iq = await u.waitUntil(() => _converse.api.connection.get().IQ_stanzas.filter(
                 iq => iq.querySelector(
@@ -2390,7 +2419,7 @@ describe("Groupchats", function () {
                     </iq>`
             _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
 
-            const view = _converse.chatboxviews.get(muc_jid);
+            const view = await u.waitUntil(() => _converse.chatboxviews.get(muc_jid));
             await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING);
 
             const presence =
@@ -2415,8 +2444,9 @@ describe("Groupchats", function () {
         it("will show an error message if the user is not allowed to have created the groupchat",
                 mock.initConverse([], {}, async function (_converse) {
 
+            const { api } = _converse;
             const muc_jid = 'impermissable@muc.montague.lit'
-            await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo')
+            api.rooms.open(muc_jid, { nick: 'romeo' });
 
             // We pretend this is a new room, so no disco info is returned.
             const iq = await u.waitUntil(() => _converse.api.connection.get().IQ_stanzas.filter(
@@ -2425,7 +2455,7 @@ describe("Groupchats", function () {
                 )).pop());
 
             const features_stanza =
-                stx`<iq from="room@conference.example.org"
+                stx`<iq from="${muc_jid}"
                         id="${iq.getAttribute('id')}"
                         to="romeo@montague.lit/desktop"
                         type="error"
@@ -2436,8 +2466,6 @@ describe("Groupchats", function () {
                 </iq>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
 
-            const view = _converse.chatboxviews.get(muc_jid);
-            await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING));
 
             const presence =
                 stx`<presence xmlns="jabber:client"
@@ -2451,6 +2479,12 @@ describe("Groupchats", function () {
                     </error>
                 </presence>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(presence));
+
+            const sent_IQs = _converse.api.connection.get().IQ_stanzas;
+            while (sent_IQs.length) sent_IQs.pop();
+            await mock.getRoomFeatures(_converse, muc_jid);
+
+            const view = await u.waitUntil(() => _converse.chatboxviews.get(muc_jid));
             const el = await u.waitUntil(() => view.querySelector('.chatroom-body converse-muc-disconnected .disconnect-msg:last-child'));
             expect(el.textContent.trim()).toBe('You are not allowed to create new groupchats.');
         }));
@@ -2458,8 +2492,9 @@ describe("Groupchats", function () {
         it("will show an error message if the groupchat doesn't yet exist",
                 mock.initConverse([], {}, async function (_converse) {
 
+            const { api } = _converse;
             const muc_jid = 'nonexistent@muc.montague.lit'
-            await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
+            api.rooms.open(muc_jid, { nick: 'romeo' });
 
             const iq = await u.waitUntil(() => _converse.api.connection.get().IQ_stanzas.filter(
                 iq => iq.querySelector(
@@ -2479,7 +2514,7 @@ describe("Groupchats", function () {
                 </iq>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
 
-            const view = _converse.chatboxviews.get(muc_jid);
+            const view = await u.waitUntil(() => _converse.chatboxviews.get(muc_jid));
             await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING));
 
             const presence =
@@ -2502,8 +2537,9 @@ describe("Groupchats", function () {
         it("will show an error message if the groupchat has reached its maximum number of participants",
                 mock.initConverse([], {}, async function (_converse) {
 
+            const { api } = _converse;
             const muc_jid = 'maxed-out@muc.montague.lit'
-            await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo')
+            api.rooms.open(muc_jid, { nick: 'romeo' });
 
             const iq = await u.waitUntil(() => _converse.api.connection.get().IQ_stanzas.filter(
                 iq => iq.querySelector(
@@ -2523,7 +2559,7 @@ describe("Groupchats", function () {
                 </iq>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
 
-            const view = _converse.chatboxviews.get(muc_jid);
+            const view = await u.waitUntil(() => _converse.chatboxviews.get(muc_jid));
             await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING));
 
             const presence =
@@ -2547,7 +2583,10 @@ describe("Groupchats", function () {
     describe("The affiliations delta", function () {
 
         it("can be computed in various ways", mock.initConverse([], {}, async function (_converse) {
-            await mock.openChatRoom(_converse, 'coven', 'chat.shakespeare.lit', 'romeo');
+            const { api } = _converse;
+            const muc_jid = 'coven@chat.shakespeare.lit';
+            api.rooms.open(muc_jid, { nick: 'romeo' });
+
             let exclude_existing = false;
             let remove_absentees = false;
             let new_list = [];

+ 102 - 97
src/plugins/muc-views/tests/nickname.js

@@ -21,7 +21,7 @@ describe("A MUC", function () {
         expect(model.occupants.at(0).get('nick')).toBe(nick);
 
         const view = _converse.chatboxviews.get(muc_jid);
-        const dropdown_item = view.querySelector(".open-nickname-modal");
+        const dropdown_item = await u.waitUntil(() => view.querySelector(".open-nickname-modal"));
         dropdown_item.click();
 
         const modal = _converse.api.modal.get('converse-muc-nickname-modal');
@@ -35,7 +35,6 @@ describe("A MUC", function () {
         modal.querySelector('input[type="submit"]')?.click();
 
         await u.waitUntil(() => !u.isVisible(modal));
-
         const { sent_stanzas } = _converse.api.connection.get();
         const sent_stanza = sent_stanzas.pop()
         expect(sent_stanza).toEqualStanza(
@@ -52,7 +51,7 @@ describe("A MUC", function () {
         _converse.api.connection.get()._dataRecv(mock.createRequest(
             stx`
             <presence
-                xmlns="jabber:server"
+                xmlns="jabber:client"
                 from='${muc_jid}/${nick}'
                 id='DC352437-C019-40EC-B590-AF29E879AF98'
                 to='${_converse.jid}'
@@ -74,7 +73,7 @@ describe("A MUC", function () {
         _converse.api.connection.get()._dataRecv(mock.createRequest(
             stx`
             <presence
-                xmlns="jabber:server"
+                xmlns="jabber:client"
                 from='${muc_jid}/${newnick}'
                 id='5B4F27A4-25ED-43F7-A699-382C6B4AFC67'
                 to='${_converse.jid}'>
@@ -247,7 +246,7 @@ describe("A MUC", function () {
 
             const IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
             const muc_jid = 'lounge@montague.lit';
-            await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
+            _converse.api.rooms.open(muc_jid);
 
             let stanza = await u.waitUntil(() => IQ_stanzas.filter(
                 iq => iq.querySelector(
@@ -267,29 +266,19 @@ describe("A MUC", function () {
                 </iq>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
 
-
-            /* <iq from='hag66@shakespeare.lit/pda'
-             *     id='getnick1'
-             *     to='coven@chat.shakespeare.lit'
-             *     type='get'>
-             * <query xmlns='http://jabber.org/protocol/disco#info'
-             *         node='x-roomuser-item'/>
-             * </iq>
-             */
             const iq = await u.waitUntil(() => IQ_stanzas.filter(
                     s => sizzle(`iq[to="${muc_jid}"] query[node="x-roomuser-item"]`, s).length
                 ).pop());
 
-            expect(Strophe.serialize(iq)).toBe(
-                `<iq from="romeo@montague.lit/orchard" id="${iq.getAttribute('id')}" to="lounge@montague.lit" `+
-                    `type="get" xmlns="jabber:client">`+
-                        `<query node="x-roomuser-item" xmlns="http://jabber.org/protocol/disco#info"/></iq>`);
+            expect(iq).toEqualStanza(stx`
+                <iq from="romeo@montague.lit/orchard" id="${iq.getAttribute('id')}" to="${muc_jid}" type="get" xmlns="jabber:client">
+                    <query node="x-roomuser-item" xmlns="http://jabber.org/protocol/disco#info"/>
+                </iq>`);
 
-            const view = _converse.chatboxviews.get('lounge@montague.lit');
             stanza = stx`
                 <iq type="result"
                     id="${iq.getAttribute("id")}"
-                    from="${view.model.get("jid")}"
+                    from="${muc_jid}"
                     to="${_converse.api.connection.get().jid}"
                     xmlns="jabber:client">
                     <query xmlns="http://jabber.org/protocol/disco#info" node="x-roomuser-item">
@@ -316,9 +305,15 @@ describe("A MUC", function () {
                         <status code="210"/>
                     </x>
                 </presence>`;
-
             _converse.api.connection.get()._dataRecv(mock.createRequest(presence));
 
+            // clear sent stanzas
+            while (IQ_stanzas.length) IQ_stanzas.pop();
+
+            // Now that the user has entered the groupchat, the features are requested again.
+            await mock.getRoomFeatures(_converse, muc_jid);
+
+            const view = await u.waitUntil(() => _converse.chatboxviews.get(muc_jid));
             await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED));
             await mock.returnMemberLists(_converse, muc_jid, [], ['member', 'admin', 'owner']);
             await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-info').length);
@@ -327,11 +322,15 @@ describe("A MUC", function () {
         }));
 
         it("will use the nickname set in the global settings if the user doesn't have a VCard nickname",
-                mock.initConverse(['chatBoxesFetched'], {'nickname': 'Benedict-Cucumberpatch'},
+                mock.initConverse(['chatBoxesFetched'], { nickname: 'Benedict-Cucumberpatch'},
                 async function (_converse) {
 
-            await mock.openChatRoomViaModal(_converse, 'roomy@muc.montague.lit');
-            const view = _converse.chatboxviews.get('roomy@muc.montague.lit');
+            const { api } = _converse;
+            const muc_jid = 'roomy@muc.montague.lit';
+            api.rooms.open(muc_jid);
+            await mock.getRoomFeatures(_converse, muc_jid);
+            await mock.waitForReservedNick(_converse, muc_jid, '');
+            const view = await u.waitUntil(() => _converse.chatboxviews.get(muc_jid));
             expect(view.model.get('nick')).toBe('Benedict-Cucumberpatch');
         }));
 
@@ -340,30 +339,9 @@ describe("A MUC", function () {
 
             const muc_jid = 'conflicted@muc.montague.lit';
             await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
-            const iq = await u.waitUntil(() => _converse.api.connection.get().IQ_stanzas.filter(
-                iq => iq.querySelector(
-                    `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
-                )).pop());
-
-            const features_stanza = stx`
-                <iq from="${muc_jid}"
-                        id="${iq.getAttribute('id')}"
-                        to="romeo@montague.lit/desktop"
-                        type="result"
-                        xmlns="jabber:client">
-                    <query xmlns="http://jabber.org/protocol/disco#info">
-                        <identity category="conference" name="A Dark Cave" type="text"/>
-                        <feature var="http://jabber.org/protocol/muc"/>
-                        <feature var="muc_hidden"/>
-                        <feature var="muc_temporary"/>
-                    </query>
-                </iq>`;
-            _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
+            await mock.getRoomFeatures(_converse, muc_jid);
 
-            const view = _converse.chatboxviews.get(muc_jid);
-            await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING);
-
-            const presence = stx`
+            _converse.api.connection.get()._dataRecv(mock.createRequest(stx`
                 <presence
                         from="${muc_jid}/romeo"
                         id="${u.getUniqueId()}"
@@ -374,53 +352,64 @@ describe("A MUC", function () {
                     <error by="${muc_jid}" type="cancel">
                         <conflict xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
                     </error>
-                </presence>`;
-            _converse.api.connection.get()._dataRecv(mock.createRequest(presence));
+                </presence>`));
 
+            const view = await u.waitUntil(() => _converse.chatboxviews.get(muc_jid));
             const el = await u.waitUntil(() => view.querySelector('.muc-nickname-form .validation-message'));
             expect(el.textContent.trim()).toBe('The nickname you chose is reserved or currently in use, please choose a different one.');
         }));
 
-
         it("will automatically choose a new nickname if a nickname conflict happens and muc_nickname_from_jid=true",
                 mock.initConverse(['chatBoxesFetched'], {vcard: { nickname: '' }}, async function (_converse) {
 
             const { api } = _converse;
             const muc_jid = 'conflicting@muc.montague.lit'
-            await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
-            /* <presence
-             *      from='coven@chat.shakespeare.lit/thirdwitch'
-             *      id='n13mt3l'
-             *      to='hag66@shakespeare.lit/pda'
-             *      type='error'>
-             *  <x xmlns='http://jabber.org/protocol/muc'/>
-             *  <error by='coven@chat.shakespeare.lit' type='cancel'>
-             *      <conflict xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
-             *  </error>
-             *  </presence>
-             */
+
             api.settings.set('muc_nickname_from_jid', true);
+            api.rooms.open(muc_jid, { nick: 'romeo' });
+            await mock.getRoomFeatures(_converse, muc_jid);
+
+            const connection = api.connection.get();
+            const sent_stanzas = connection.sent_stanzas;
+            await u.waitUntil(() => sent_stanzas.filter(iq => sizzle('presence history', iq).length).pop());
+
+            const { IQ_stanzas } = api.connection.get();
+
+            while (IQ_stanzas.length) IQ_stanzas.pop();
+            while (sent_stanzas.length) sent_stanzas.pop();
 
+            // Simulate repeatedly that there's already someone in the groupchat
+            // with that nickname
             let presence = stx`
-                <presence
-                        xmlns="jabber:client"
+                <presence xmlns="jabber:client"
                         from='${muc_jid}/romeo'
                         id='${u.getUniqueId()}'
-                        to='romeo@montague.lit/pda'
+                        to='${api.connection.get().jid}'
                         type='error'>
                     <x xmlns='http://jabber.org/protocol/muc'/>
                     <error by='${muc_jid}' type='cancel'>
                         <conflict xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
                     </error>
                 </presence>`;
+            api.connection.get()._dataRecv(mock.createRequest(presence));
 
-            const view = _converse.chatboxviews.get(muc_jid);
-            spyOn(view.model, 'join').and.callThrough();
+            await mock.getRoomFeatures(_converse, muc_jid);
 
-            // Simulate repeatedly that there's already someone in the groupchat
-            // with that nickname
-            _converse.api.connection.get()._dataRecv(mock.createRequest(presence));
-            expect(view.model.join).toHaveBeenCalledWith('romeo-2');
+            let sent_stanza = await u.waitUntil(() => sent_stanzas.filter(iq => sizzle('presence history', iq).length).pop());
+            expect(sent_stanza).toEqualStanza(stx`
+                <presence id="${sent_stanza.getAttribute('id')}"
+                        from="${connection.jid}"
+                        to="${muc_jid}/romeo-2"
+                        xmlns="jabber:client">
+                    <x xmlns="http://jabber.org/protocol/muc">
+                        <history maxstanzas="0"/>
+                    </x>
+                    <c xmlns="http://jabber.org/protocol/caps" hash="sha-1" node="https://conversejs.org"
+                        ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ="/>
+                </presence>`);
+
+            while (IQ_stanzas.length) IQ_stanzas.pop();
+            while (sent_stanzas.length) sent_stanzas.pop();
 
             presence = stx`
                 <presence
@@ -436,7 +425,23 @@ describe("A MUC", function () {
                 </presence>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(presence));
 
-            expect(view.model.join).toHaveBeenCalledWith('romeo-3');
+            await mock.getRoomFeatures(_converse, muc_jid);
+
+            sent_stanza = await u.waitUntil(() => sent_stanzas.filter(iq => sizzle('presence history', iq).length).pop());
+            expect(sent_stanza).toEqualStanza(stx`
+                <presence id="${sent_stanza.getAttribute('id')}"
+                        from="${connection.jid}"
+                        to="${muc_jid}/romeo-3"
+                        xmlns="jabber:client">
+                    <x xmlns="http://jabber.org/protocol/muc">
+                        <history maxstanzas="0"/>
+                    </x>
+                    <c xmlns="http://jabber.org/protocol/caps" hash="sha-1" node="https://conversejs.org"
+                        ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ="/>
+                </presence>`);
+
+            while (IQ_stanzas.length) IQ_stanzas.pop();
+            while (sent_stanzas.length) sent_stanzas.pop();
 
             presence = stx`
                 <presence
@@ -451,34 +456,30 @@ describe("A MUC", function () {
                     </error>
                 </presence>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(presence));
-            expect(view.model.join).toHaveBeenCalledWith('romeo-4');
+
+            await mock.getRoomFeatures(_converse, muc_jid);
+
+            sent_stanza = await u.waitUntil(() => sent_stanzas.filter(iq => sizzle('presence history', iq).length).pop());
+            expect(sent_stanza).toEqualStanza(stx`
+                <presence id="${sent_stanza.getAttribute('id')}"
+                        from="${connection.jid}"
+                        to="${muc_jid}/romeo-4"
+                        xmlns="jabber:client">
+                    <x xmlns="http://jabber.org/protocol/muc">
+                        <history maxstanzas="0"/>
+                    </x>
+                    <c xmlns="http://jabber.org/protocol/caps" hash="sha-1" node="https://conversejs.org"
+                        ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ="/>
+                </presence>`);
         }));
 
         it("will show an error message if the user's nickname doesn't conform to groupchat policy",
                 mock.initConverse([], {}, async function (_converse) {
 
+            const { api } = _converse;
             const muc_jid = 'conformist@muc.montague.lit'
-            await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
-
-            const iq = await u.waitUntil(() => _converse.api.connection.get().IQ_stanzas.filter(
-                iq => iq.querySelector(
-                    `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
-                )).pop());
-            const features_stanza = stx`
-                <iq from="${muc_jid}"
-                        id="${iq.getAttribute('id')}"
-                        to="romeo@montague.lit/desktop"
-                        type="result"
-                        xmlns="jabber:client">
-                    <query xmlns="http://jabber.org/protocol/disco#info">
-                        <identity category="conference" name="A Dark Cave" type="text"/>
-                        <feature var="http://jabber.org/protocol/muc"/>
-                    </query>
-                </iq>`;
-            _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
-
-            const view = _converse.chatboxviews.get(muc_jid);
-            await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING));
+            api.rooms.open(muc_jid, { nick: 'romeo' });
+            await mock.getRoomFeatures(_converse, muc_jid);
 
             const presence = stx`
                 <presence
@@ -492,8 +493,9 @@ describe("A MUC", function () {
                         <not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
                     </error>
                 </presence>`;
+            api.connection.get()._dataRecv(mock.createRequest(presence));
 
-            _converse.api.connection.get()._dataRecv(mock.createRequest(presence));
+            const view = await u.waitUntil(() => _converse.chatboxviews.get(muc_jid));
             const el = await u.waitUntil(() => view.querySelector('.chatroom-body converse-muc-disconnected .disconnect-msg:last-child'));
             expect(el.textContent.trim()).toBe("Your nickname doesn't conform to this groupchat's policies.");
         }));
@@ -505,6 +507,7 @@ describe("A MUC", function () {
                 vcard: { nickname: '' },
             }, async function (_converse) {
 
+            const muc_jid = 'lounge@montague.lit';
             await mock.openControlBox(_converse);
             await mock.waitForRoster(_converse, 'current', 0);
             const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
@@ -513,12 +516,14 @@ describe("A MUC", function () {
             const modal = _converse.api.modal.get('converse-add-muc-modal');
             await u.waitUntil(() => u.isVisible(modal), 1000)
             const name_input = modal.querySelector('input[name="chatroom"]');
-            name_input.value = 'lounge@montague.lit';
+            name_input.value = muc_jid;
             expect(modal.querySelector('label[for="nickname"]')).toBe(null);
             expect(modal.querySelector('input[name="nickname"]')).toBe(null);
             modal.querySelector('form input[type="submit"]').click();
+
+            await mock.getRoomFeatures(_converse, muc_jid);
             await u.waitUntil(() => _converse.chatboxes.length > 1);
-            const chatroom = _converse.chatboxes.get('lounge@montague.lit');
+            const chatroom = _converse.chatboxes.get(muc_jid);
             expect(chatroom.get('nick')).toBe('romeo');
         }));
 

+ 1 - 1
src/plugins/muc-views/tests/rai.js

@@ -136,7 +136,7 @@ describe("XEP-0437 Room Activity Indicators", function () {
         const nick = 'romeo';
         const sent_stanzas = _converse.api.connection.get().sent_stanzas;
 
-        const muc_creation_promise = await api.rooms.open(muc_jid, { nick }, false);
+        const muc_creation_promise = api.rooms.open(muc_jid, { nick }, false);
         await mock.getRoomFeatures(_converse, muc_jid, []);
         await mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
         await muc_creation_promise;

+ 17 - 7
src/plugins/roomslist/tests/grouplists.js

@@ -2,7 +2,6 @@
 
 const { u } = converse.env;
 
-
 describe("The list of MUC domains", function () {
     it("is shown in controlbox", mock.initConverse(
             ['chatBoxesFetched'],
@@ -16,7 +15,10 @@ describe("The list of MUC domains", function () {
         const controlbox = _converse.chatboxviews.get('controlbox');
         let list = controlbox.querySelector('.list-container--openrooms');
         expect(u.hasClass('hidden', list)).toBeTruthy();
-        await mock.openChatRoom(_converse, 'room', 'conference.shakespeare.lit', 'JC');
+
+        let muc_jid = 'room@conference.shakespeare.lit';
+        _converse.api.rooms.open(muc_jid, { nick: 'JC' });
+        await mock.getRoomFeatures(_converse, muc_jid);
 
         const lview = controlbox.querySelector('converse-rooms-list');
         // Check that the group is shown
@@ -32,11 +34,14 @@ describe("The list of MUC domains", function () {
         await u.waitUntil(() => lview.querySelectorAll(".open-room").length);
         let room_els = lview.querySelectorAll(".open-room");
         expect(room_els.length).toBe(1);
-        expect(room_els[0].querySelector('span').innerText).toBe('room@conference.shakespeare.lit');
+        expect(room_els[0].querySelector('span').innerText).toBe('Room');
 
         // Check that a second room in the same domain is shown in the same
         // domain group.
-        await mock.openChatRoom(_converse, 'secondroom', 'conference.shakespeare.lit', 'JC');
+        muc_jid = 'secondroom@conference.shakespeare.lit';
+        _converse.api.rooms.open(muc_jid, { nick: 'JC' });
+        await mock.getRoomFeatures(_converse, muc_jid);
+
         await u.waitUntil(() => lview.querySelectorAll(".open-room").length > 1);
         group_els = lview.querySelectorAll(".muc-domain-group");
         expect(group_els.length).toBe(1); // still only one group
@@ -44,8 +49,10 @@ describe("The list of MUC domains", function () {
         room_els = lview.querySelectorAll(".open-room");
         expect(room_els.length).toBe(2); // but two rooms inside it
 
+        muc_jid = 'lounge@montague.lit';
+        _converse.api.rooms.open(muc_jid, { nick: 'romeo' });
+        await mock.getRoomFeatures(_converse, muc_jid);
 
-        await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
         await u.waitUntil(() => lview.querySelectorAll(".open-room").length > 2);
         room_els = lview.querySelectorAll(".open-room");
         expect(room_els.length).toBe(3);
@@ -64,7 +71,7 @@ describe("The list of MUC domains", function () {
         expect(room_els.length).toBe(1);
         group_els = lview.querySelectorAll(".muc-domain-group");
         expect(group_els.length).toBe(1);
-        expect(room_els[0].querySelector('span').innerText).toBe('lounge@montague.lit');
+        expect(room_els[0].querySelector('span').innerText).toBe('Lounge');
         expect(group_els[0].children[0].innerText.trim()).toBe('montague.lit');
         list = controlbox.querySelector('.list-container--openrooms');
         u.waitUntil(() => Array.from(list.classList).includes('hidden'));
@@ -93,7 +100,10 @@ describe("A MUC domain group", function () {
         await mock.openControlBox(_converse);
         const controlbox = _converse.chatboxviews.get('controlbox');
         const list = controlbox.querySelector('.list-container--openrooms');
-        await mock.openChatRoom(_converse, 'room', 'conference.shakespeare.lit', 'JC');
+        const nick = 'JC';
+        const muc_jid = 'room@conference.shakespeare.lit';
+        _converse.api.rooms.open(muc_jid, { nick });
+        await mock.getRoomFeatures(_converse, muc_jid);
 
         const lview = controlbox.querySelector('converse-rooms-list');
         await u.waitUntil(() => lview.querySelectorAll(".muc-domain-group").length);

+ 27 - 11
src/plugins/roomslist/tests/roomslist.js

@@ -13,20 +13,27 @@ describe("A list of open groupchats", function () {
                                         // have to mock stanza traffic.
             }, async function (_converse) {
 
+        const { api } = _converse;
         await mock.waitForRoster(_converse, 'current', 0);
         await mock.openControlBox(_converse);
         const controlbox = _converse.chatboxviews.get('controlbox');
         let list = controlbox.querySelector('.list-container--openrooms');
         expect(u.hasClass('hidden', list)).toBeTruthy();
-        await mock.openChatRoom(_converse, 'room', 'conference.shakespeare.lit', 'JC');
+
+        let muc_jid = 'room@conference.shakespeare.lit';
+        api.rooms.open(muc_jid, { nick: 'romeo' });
+        await mock.getRoomFeatures(_converse, muc_jid);
 
         const lview = controlbox.querySelector('converse-rooms-list');
         await u.waitUntil(() => lview.querySelectorAll(".open-room").length);
         let room_els = lview.querySelectorAll(".open-room");
         expect(room_els.length).toBe(1);
-        expect(room_els[0].querySelector('span').innerText).toBe('room@conference.shakespeare.lit');
+        expect(room_els[0].querySelector('span').innerText).toBe('Room');
+
+        muc_jid = 'lounge@montague.lit';
+        api.rooms.open(muc_jid, { nick: 'romeo' });
+        await mock.getRoomFeatures(_converse, muc_jid);
 
-        await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
         await u.waitUntil(() => lview.querySelectorAll(".open-room").length > 1);
         room_els = lview.querySelectorAll(".open-room");
         expect(room_els.length).toBe(2);
@@ -35,7 +42,7 @@ describe("A list of open groupchats", function () {
         await view.close();
         room_els = lview.querySelectorAll(".open-room");
         expect(room_els.length).toBe(1);
-        expect(room_els[0].querySelector('span').innerText).toBe('lounge@montague.lit');
+        expect(room_els[0].querySelector('span').innerText).toBe('Lounge');
         list = controlbox.querySelector('.list-container--openrooms');
         u.waitUntil(() => Array.from(list.classList).includes('hidden'));
 
@@ -108,7 +115,10 @@ describe("A groupchat shown in the groupchats list", function () {
         const controlbox = _converse.chatboxviews.get('controlbox');
         const u = converse.env.utils;
         const muc_jid = 'coven@chat.shakespeare.lit';
-        await _converse.api.rooms.open(muc_jid, {'nick': 'some1'}, true);
+
+        _converse.api.rooms.open(muc_jid, {'nick': 'some1'}, true);
+        await mock.getRoomFeatures(_converse, muc_jid);
+
         const lview = controlbox.querySelector('converse-rooms-list');
         await u.waitUntil(() => lview.querySelectorAll(".open-room").length);
         let room_els = lview.querySelectorAll(".available-chatroom");
@@ -117,8 +127,11 @@ describe("A groupchat shown in the groupchats list", function () {
         let item = room_els[0];
         await u.waitUntil(() => _converse.chatboxes.get(muc_jid).get('hidden') === false);
         await u.waitUntil(() => u.hasClass('open', item), 1000);
-        expect(item.querySelector('.open-room span').textContent.trim()).toBe('coven@chat.shakespeare.lit');
-        await _converse.api.rooms.open('balcony@chat.shakespeare.lit', {'nick': 'some1'}, true);
+        expect(item.querySelector('.open-room span').textContent.trim()).toBe('Coven');
+
+        _converse.api.rooms.open('balcony@chat.shakespeare.lit', {'nick': 'some1'}, true);
+        await mock.getRoomFeatures(_converse, 'balcony@chat.shakespeare.lit');
+
         await u.waitUntil(() => lview.querySelectorAll(".open-room").length > 1);
         room_els = lview.querySelectorAll(".open-room");
         expect(room_els.length).toBe(2);
@@ -126,7 +139,7 @@ describe("A groupchat shown in the groupchats list", function () {
         room_els = lview.querySelectorAll(".available-chatroom.open");
         expect(room_els.length).toBe(1);
         item = room_els[0];
-        expect(item.querySelector('.open-room span').textContent.trim()).toBe('balcony@chat.shakespeare.lit');
+        expect(item.querySelector('.open-room span').textContent.trim()).toBe('Balcony');
     }));
 
     it("shows the MUC avatar", mock.initConverse(
@@ -268,8 +281,11 @@ describe("A groupchat shown in the groupchats list", function () {
         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');
-        expect(_converse.chatboxes.length).toBe(2);
+        const muc_jid = 'lounge@conference.shakespeare.lit';
+        _converse.api.rooms.open(muc_jid, { nick: 'romeo' });
+        await mock.getRoomFeatures(_converse, muc_jid);
+
+        await u.waitUntil(() => _converse.chatboxes.length === 2);
 
         await mock.openControlBox(_converse);
         const controlbox = _converse.chatboxviews.get('controlbox');
@@ -281,7 +297,7 @@ describe("A groupchat shown in the groupchats list", function () {
         const close_el = rooms_list.querySelector(".close-room");
         close_el.click();
         expect(_converse.api.confirm).toHaveBeenCalledWith(
-            'Are you sure you want to leave the groupchat lounge@conference.shakespeare.lit?');
+            'Are you sure you want to leave the groupchat Lounge?');
 
         await u.waitUntil(() => rooms_list.querySelectorAll(".open-room").length === 0);
         expect(_converse.chatboxes.length).toBe(1);

+ 30 - 10
src/shared/tests/mock.js

@@ -215,26 +215,45 @@ async function openChatBoxFor (_converse, jid) {
     return u.waitUntil(() => _converse.chatboxviews.get(jid), 1000);
 }
 
-async function openChatRoomViaModal (_converse, jid, nick='') {
-    // Opens a new chatroom
-    const model = await _converse.api.controlbox.open('controlbox');
-    await u.waitUntil(() => model.get('connected'));
+async function openChatRoomViaModal (_converse, muc_jid, nick='') {
+    const controlbox = await _converse.api.controlbox.open('controlbox');
+    await u.waitUntil(() => controlbox.get('connected'));
     await openControlBox(_converse);
+
     document.querySelector('converse-rooms-list .show-add-muc-modal').click();
     closeControlBox(_converse);
     const modal = _converse.api.modal.get('converse-add-muc-modal');
     await u.waitUntil(() => u.isVisible(modal), 1500)
-    modal.querySelector('input[name="chatroom"]').value = jid;
+    modal.querySelector('input[name="chatroom"]').value = muc_jid;
     if (nick) {
         modal.querySelector('input[name="nickname"]').value = nick;
     }
     modal.querySelector('form input[type="submit"]').click();
-    await u.waitUntil(() => _converse.chatboxviews.get(jid), 1000);
-    return _converse.chatboxviews.get(jid);
+    await mock.getRoomFeatures(_converse, muc_jid);
+    if (!nick) await mock.waitForReservedNick(_converse, muc_jid, '');
 }
 
-function openChatRoom (_converse, room, server) {
-    return _converse.api.rooms.open(`${room}@${server}`);
+async function waitOnDiscoInfoForNewMUC(_converse, muc_jid) {
+    const { api } = _converse;
+    const connection = api.connection.get();
+    const own_jid = connection.jid;
+    const stanzas = connection.IQ_stanzas;
+    const stanza = await u.waitUntil(() => stanzas.filter(
+        iq => iq.querySelector(
+            `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
+        )).pop()
+    );
+    const features_stanza =
+        stx`<iq from="${muc_jid}"
+                id="${stanza.getAttribute('id')}"
+                to="${own_jid}"
+                type="error"
+                xmlns="jabber:client">
+            <error type="cancel">
+                <item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
+            </error>
+        </iq>`;
+    _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
 }
 
 async function getRoomFeatures (_converse, muc_jid, features=[], settings={}) {
@@ -409,6 +428,7 @@ async function openAndEnterChatRoom (
     ) {
     const { api } = _converse;
     muc_jid = muc_jid.toLowerCase();
+
     const room_creation_promise = api.rooms.open(muc_jid, settings, force_open);
     await getRoomFeatures(_converse, muc_jid, features, settings);
     await waitForReservedNick(_converse, muc_jid, nick);
@@ -890,7 +910,6 @@ Object.assign(mock, {
     openAndEnterChatRoom,
     openChatBoxFor,
     openChatBoxes,
-    openChatRoom,
     openChatRoomViaModal,
     openControlBox,
     ownDeviceHasBeenPublished,
@@ -903,6 +922,7 @@ Object.assign(mock, {
     view_mode,
     waitForReservedNick,
     waitForRoster,
+    waitOnDiscoInfoForNewMUC,
     waitUntilBlocklistInitialized,
     waitUntilBookmarksReturned,
     waitUntilDiscoConfirmed

+ 4 - 0
src/types/plugins/muc-views/heading.d.ts

@@ -18,6 +18,10 @@ export default class MUCHeading extends CustomElement {
      * @param {Event} ev
      */
     showRoomDetailsModal(ev: Event): void;
+    /**
+     * @param {Event} ev
+     */
+    showNicknameModal(ev: Event): void;
     /**
      * @param {Event} ev
      */

+ 8 - 2
src/types/plugins/muc-views/nickname-form.d.ts

@@ -6,10 +6,16 @@ declare class MUCNicknameForm extends CustomElement {
         };
     };
     jid: any;
-    connectedCallback(): void;
     model: any;
+    /**
+     * @param {Map<string, any>} changed
+     */
+    shouldUpdate(changed: Map<string, any>): boolean;
     render(): import("lit").TemplateResult<1>;
-    submitNickname(ev: any): void;
+    /**
+     * @param {Event} ev
+     */
+    submitNickname(ev: Event): void;
     closeModal(): void;
 }
 import { CustomElement } from 'shared/components/element';

+ 1 - 1
src/types/plugins/muc-views/templates/muc-nickname-form.d.ts

@@ -1,3 +1,3 @@
-declare function _default(el: any): import("lit").TemplateResult<1>;
+declare function _default(el: import("../nickname-form").default): import("lit").TemplateResult<1>;
 export default _default;
 //# sourceMappingURL=muc-nickname-form.d.ts.map