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

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

@@ -396,7 +396,7 @@ export default {
                 entity.queryInfo();
                 entity.queryInfo();
             } else {
             } else {
                 // Create it if it doesn't exist
                 // 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;
             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 _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import api from '../../shared/api/index.js';
 import converse from '../../shared/api/public.js';
 import converse from '../../shared/api/public.js';
+import { parseErrorStanza } from '../../shared/parsers.js';
 import log from '../../log.js';
 import log from '../../log.js';
 import sizzle from 'sizzle';
 import sizzle from 'sizzle';
-import { Collection, Model } from '@converse/skeletor';
-import { getOpenPromise } from '@converse/openpromise';
 import { createStore } from '../../utils/storage.js';
 import { createStore } from '../../utils/storage.js';
 
 
-const { Strophe } = converse.env;
+const { Strophe, u } = converse.env;
 
 
 /**
 /**
  * @class
  * @class
@@ -126,7 +127,7 @@ class DiscoEntity extends Model {
             stanza = await api.disco.info(this.get('jid'), null);
             stanza = await api.disco.info(this.get('jid'), null);
         } catch (iq) {
         } catch (iq) {
             iq === null ? log.error(`Timeout for disco#info query for ${this.get('jid')}`) : log.error(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;
             return;
         }
         }
         this.onInfo(stanza);
         this.onInfo(stanza);

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

@@ -23,7 +23,7 @@ import {
 } from './constants.js';
 } from './constants.js';
 import { CHATROOMS_TYPE, GONE, INACTIVE, METADATA_ATTRIBUTES } from '../../shared/constants.js';
 import { CHATROOMS_TYPE, GONE, INACTIVE, METADATA_ATTRIBUTES } from '../../shared/constants.js';
 import { Strophe, $build, $iq, $msg, $pres } from 'strophe.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 { computeAffiliationsDelta, setAffiliations, getAffiliationList } from './affiliations/utils.js';
 import { initStorage, createStore } from '../../utils/storage.js';
 import { initStorage, createStore } from '../../utils/storage.js';
 import { isArchived, parseErrorStanza } from '../../shared/parsers.js';
 import { isArchived, parseErrorStanza } from '../../shared/parsers.js';
@@ -116,7 +116,7 @@ class MUC extends ModelWithMessages(ColorAwareModel(ChatBoxBase)) {
 
 
         const restored = await this.restoreFromCache();
         const restored = await this.restoreFromCache();
         if (!restored) {
         if (!restored) {
-            this.join();
+            await this.join();
         }
         }
         /**
         /**
          * Triggered once a {@link MUC} has been created and initialized.
          * 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.
      * @param {String} [password] - Optional password, if required by the groupchat.
      *  Will fall back to the `password` value stored in the room
      *  Will fall back to the `password` value stored in the room
      *  model (if available).
      *  model (if available).
+     *  @returns {Promise<void>}
      */
      */
     async join (nick, password) {
     async join (nick, password) {
         if (this.isEntered()) {
         if (this.isEntered()) {
             // We have restored a groupchat from session storage,
             // We have restored a groupchat from session storage,
             // so we don't send out a presence stanza again.
             // so we don't send out a presence stanza again.
-            return this;
+            return;
         }
         }
         // Set this early, so we don't rejoin in onHiddenChange
         // Set this early, so we don't rejoin in onHiddenChange
         this.session.save('connection_status', ROOMSTATUS.CONNECTING);
         this.session.save('connection_status', ROOMSTATUS.CONNECTING);
-        await this.refreshDiscoInfo();
+
+        const is_new = (await this.refreshDiscoInfo() instanceof ItemNotFoundError);
         nick = await this.getAndPersistNickname(nick);
         nick = await this.getAndPersistNickname(nick);
         if (!nick) {
         if (!nick) {
             safeSave(this.session, { 'connection_status': ROOMSTATUS.NICKNAME_REQUIRED });
             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();
                 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 {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({
         let stanza = $pres({
             'id': getUniqueId(),
             'id': getUniqueId(),
             'from': api.connection.get().jid,
             'from': api.connection.get().jid,
             'to': this.getRoomJIDAndNick()
             'to': this.getRoomJIDAndNick()
         }).c('x', { 'xmlns': Strophe.NS.MUC })
         }).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');
         password = password || this.get('password');
         if (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}.
      * *fields* are stored on the config {@link Model} attribute on this {@link MUC}.
      * @returns {Promise}
      * @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>}
  * @returns {Promise<import('./types').MUCPresenceAttributes>}
  */
  */
 export async function parseMUCPresence(stanza, chatbox) {
 export async function parseMUCPresence(stanza, chatbox) {
+    await chatbox.initialized;
+
     /**
     /**
      * @typedef {import('./types').MUCPresenceAttributes} MUCPresenceAttributes
      * @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 () {
 describe("Groupchats", function () {
 
 
+    beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
+
     it("keeps track of unread messages and mentions",
     it("keeps track of unread messages and mentions",
             mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
             mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
 
 
@@ -51,12 +53,12 @@ describe("Groupchats", function () {
             const muc = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
             const muc = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
 
 
             let pres = await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'presence').pop());
             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');
             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);
         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",
     it("will be added to a MUC message based on the XEP-0421 occupant id",
             mock.initConverse([], {}, async function (_converse) {
             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);
     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
  * are correct, for example that the "type" is set to
  * "chatroom".
  * "chatroom".
  * @param {string} jid
  * @param {string} jid
@@ -150,7 +151,7 @@ export async function onDirectMUCInvitation (message) {
     }
     }
 
 
     if (result) {
     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) {
         if (chatroom.session.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED) {
             _converse.state.chatboxes.get(room_jid).rejoin();
             _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;
     public vcard: import("../vcard/vcard").default;
     initialized: any;
     initialized: any;
-    debouncedRejoin: import("lodash").DebouncedFunc<() => Promise<this>>;
+    debouncedRejoin: import("lodash").DebouncedFunc<() => Promise<void>>;
     isEntered(): boolean;
     isEntered(): boolean;
     /**
     /**
      * Checks whether this MUC qualifies for subscribing to XEP-0437 Room Activity Indicators (RAI)
      * 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.
      * @param {String} [password] - Optional password, if required by the groupchat.
      *  Will fall back to the `password` value stored in the room
      *  Will fall back to the `password` value stored in the room
      *  model (if available).
      *  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.
      * Clear stale cache and re-join a MUC we've been in before.
      */
      */
-    rejoin(): Promise<this>;
+    rejoin(): Promise<void>;
     /**
     /**
      * @param {string} password
      * @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;
     clearOccupantsCache(): void;
     /**
     /**
      * Given the passed in MUC message, send a XEP-0333 chat marker.
      * 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.
      *  to update the list.
      */
      */
     updateMemberLists(members: object): Promise<any>;
     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
      * Given a nick name, save it to the model state, otherwise, look
      * for a server-side reserved nickname or default configured
      * 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]
  * @param {Event} [event]
  */
  */
 export function routeToRoom(event?: Event): Promise<void>;
 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
  * A direct MUC invitation to join a groupchat has been received
  * See XEP-0249: Direct MUC invitations.
  * 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);
         await u.waitUntil(() => modal.querySelectorAll('.bookmarks.rooms-list .room-item').length);
         expect(modal.querySelectorAll('.bookmarks.rooms-list .room-item').length).toBe(2);
         expect(modal.querySelectorAll('.bookmarks.rooms-list .room-item').length).toBe(2);
         modal.querySelector('.bookmarks.rooms-list .open-room').click();
         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);
         await u.waitUntil(() => _converse.chatboxes.length === 2);
         expect((await api.rooms.get('first@conference.shakespeare.lit')).get('hidden')).toBe(false);
         expect((await api.rooms.get('first@conference.shakespeare.lit')).get('hidden')).toBe(false);
 
 
         await u.waitUntil(() => modal.querySelectorAll('.list-container--bookmarks .available-chatroom').length);
         await u.waitUntil(() => modal.querySelectorAll('.list-container--bookmarks .available-chatroom').length);
         modal.querySelector('.list-container--bookmarks .available-chatroom:last-child .open-room').click();
         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);
         await u.waitUntil(() => _converse.chatboxes.length === 3);
 
 
         expect((await api.rooms.get('first@conference.shakespeare.lit')).get('hidden')).toBe(true);
         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'
                 'nick': 'Othello'
             });
             });
             spyOn(_converse.ChatRoom.prototype, 'getAndPersistNickname').and.callThrough();
             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);
             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');
             expect(room.get('nick')).toBe('Othello');
         }));
         }));
     });
     });
@@ -36,6 +36,7 @@ describe("Bookmarks", function () {
     it("can be pushed from the XMPP server", mock.initConverse(
     it("can be pushed from the XMPP server", mock.initConverse(
             ['connected', 'chatBoxesFetched'], {}, async function (_converse) {
             ['connected', 'chatBoxesFetched'], {}, async function (_converse) {
 
 
+        const { api } = _converse;
         const { u } = converse.env;
         const { u } = converse.env;
         await mock.waitForRoster(_converse, 'current', 0);
         await mock.waitForRoster(_converse, 'current', 0);
         await mock.waitUntilBookmarksReturned(_converse);
         await mock.waitUntilBookmarksReturned(_converse);
@@ -67,12 +68,13 @@ describe("Bookmarks", function () {
             </event>
             </event>
         </message>`;
         </message>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
+        await mock.getRoomFeatures(_converse, 'theplay@conference.shakespeare.lit');
 
 
         const { bookmarks } = _converse.state;
         const { bookmarks } = _converse.state;
         await u.waitUntil(() => bookmarks.length);
         await u.waitUntil(() => bookmarks.length);
         expect(bookmarks.length).toBe(2);
         expect(bookmarks.length).toBe(2);
         expect(bookmarks.map(b => b.get('name'))).toEqual(['Another bookmark', "The Play's the Thing"]);
         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"
         stanza = stx`<message from="romeo@montague.lit"
                         to="${_converse.jid}"
                         to="${_converse.jid}"
@@ -247,10 +249,11 @@ describe("Bookmarks", function () {
         expect(theplay.get('name')).toBe("The Play's the Thing");
         expect(theplay.get('name')).toBe("The Play's the Thing");
         expect(theplay.get('nick')).toBe('JC');
         expect(theplay.get('nick')).toBe('JC');
         expect(theplay.get('password')).toBe('secret');
         expect(theplay.get('password')).toBe('secret');
-
         expect(bookmarks.get('orchard@conference.shakespeare.lit').get('autojoin')).toBe(false);
         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));
         await u.waitUntil(() => _converse.state.chatboxes.get(autojoin_muc));
+
         const features = [
         const features = [
             'http://jabber.org/protocol/muc',
             'http://jabber.org/protocol/muc',
             'jabber:iq:register',
             'jabber:iq:register',
@@ -269,7 +272,7 @@ describe("Bookmarks", function () {
                 id="${sent_stanza.getAttribute('id')}"
                 id="${sent_stanza.getAttribute('id')}"
                 to="${autojoin_muc}/JC">
                 to="${autojoin_muc}/JC">
             <x xmlns="http://jabber.org/protocol/muc">
             <x xmlns="http://jabber.org/protocol/muc">
-                <history/>
+                <history maxstanzas="0"/>
                 <password>secret</password>
                 <password>secret</password>
             </x>
             </x>
             <c xmlns="http://jabber.org/protocol/caps"
             <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(
     it("can be pushed from the XMPP server", mock.initConverse(
             ['connected', 'chatBoxesFetched'], {}, async function (_converse) {
             ['connected', 'chatBoxesFetched'], {}, async function (_converse) {
 
 
+        const { api } = _converse;
         const { u } = converse.env;
         const { u } = converse.env;
         await mock.waitForRoster(_converse, 'current', 0);
         await mock.waitForRoster(_converse, 'current', 0);
         await mock.waitUntilBookmarksReturned(
         await mock.waitUntilBookmarksReturned(
@@ -37,12 +38,13 @@ describe("Bookmarks", function () {
             </event>
             </event>
         </message>`;
         </message>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
+        await mock.getRoomFeatures(_converse, 'theplay@conference.shakespeare.lit');
 
 
         const { bookmarks } = _converse.state;
         const { bookmarks } = _converse.state;
         await u.waitUntil(() => bookmarks.length);
         await u.waitUntil(() => bookmarks.length);
         expect(bookmarks.length).toBe(2);
         expect(bookmarks.length).toBe(2);
         expect(bookmarks.map(b => b.get('name'))).toEqual(['Another bookmark', "The Play's the Thing"]);
         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">
         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'>
             <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);
         await u.waitUntil(() => modal.querySelectorAll('.bookmarks.rooms-list .room-item').length);
         expect(modal.querySelectorAll('.bookmarks.rooms-list .room-item').length).toBe(2);
         expect(modal.querySelectorAll('.bookmarks.rooms-list .room-item').length).toBe(2);
         modal.querySelector('.bookmarks.rooms-list .open-room').click();
         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);
         await u.waitUntil(() => _converse.chatboxes.length === 2);
         expect((await api.rooms.get('first@conference.shakespeare.lit')).get('hidden')).toBe(false);
         expect((await api.rooms.get('first@conference.shakespeare.lit')).get('hidden')).toBe(false);
 
 
         await u.waitUntil(() => modal.querySelectorAll('.list-container--bookmarks .available-chatroom').length);
         await u.waitUntil(() => modal.querySelectorAll('.list-container--bookmarks .available-chatroom').length);
         modal.querySelector('.list-container--bookmarks .available-chatroom:last-child .open-room').click();
         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);
         await u.waitUntil(() => _converse.chatboxes.length === 3);
 
 
         expect((await api.rooms.get('first@conference.shakespeare.lit')).get('hidden')).toBe(true);
         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 { api } = _converse;
         const muc_jid = 'lounge@montague.lit';
         const muc_jid = 'lounge@montague.lit';
         const nick = 'romeo';
         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.getRoomFeatures(_converse, muc_jid, []);
         await mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
         await mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
+
         await muc_creation_promise;
         await muc_creation_promise;
         const model = _converse.chatboxes.get(muc_jid);
         const model = _converse.chatboxes.get(muc_jid);
         await u.waitUntil(() => (model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED));
         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
                     Strophe.NS.SID
                 ];
                 ];
                 const nick = 'romeo';
                 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.getRoomFeatures(_converse, muc_jid, features);
                 await mock.waitForReservedNick(_converse, muc_jid, nick);
                 await mock.waitForReservedNick(_converse, muc_jid, nick);
                 mock.receiveOwnMUCPresence(_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));
                 await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED));
 
 
                 // Check in reverse order that we requested all three lists
                 // 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"/>`+
                 `<query xmlns="http://jabber.org/protocol/disco#info"/>`+
             `</iq>`);
             `</iq>`);
 
 
-        // State that the chat is members-only via the features IQ
-        const view = _converse.chatboxviews.get(muc_jid);
         const features_stanza = stx`
         const features_stanza = stx`
             <iq from="coven@chat.shakespeare.lit"
             <iq from="coven@chat.shakespeare.lit"
                 id="${stanza.getAttribute('id')}"
                 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));
         _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
         const sent_stanzas = _converse.api.connection.get().sent_stanzas;
         const sent_stanzas = _converse.api.connection.get().sent_stanzas;
         await u.waitUntil(() => sent_stanzas.filter(s => s.matches(`presence[to="${muc_jid}/${nick}"]`)).pop());
         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();
         expect(view.model.features.get('membersonly')).toBeTruthy();
 
 
         await room_creation_promise;
         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) {
         mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
             const modal = await mock.openAddMUCModal(_converse);
             const modal = await mock.openAddMUCModal(_converse);
 
 
+            const muc_jid = 'lounge@muc.montague.lit';
+
             let label_name = modal.querySelector('label[for="chatroom"]');
             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:');
             expect(label_name.textContent.trim()).toBe('Groupchat name or address:');
             const label_nick = modal.querySelector('label[for="nickname"]');
             const label_nick = modal.querySelector('label[for="nickname"]');
             expect(label_nick.textContent.trim()).toBe('Nickname:');
             expect(label_nick.textContent.trim()).toBe('Nickname:');
@@ -17,19 +20,19 @@ describe('The "Groupchats" Add modal', function () {
             expect(nick_input.value).toBe('Romeo');
             expect(nick_input.value).toBe('Romeo');
             nick_input.value = '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();
             modal.querySelector('form input[type="submit"]').click();
+
+            await mock.getRoomFeatures(_converse, muc_jid);
             await u.waitUntil(() => _converse.chatboxes.length);
             await u.waitUntil(() => _converse.chatboxes.length);
             await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1);
             await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1);
         })
         })
     );
     );
 
 
     it("doesn't require the domain when muc_domain is set",
     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');
             expect(modal.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat');
             spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
             spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
             const label_name = modal.querySelector('label[for="chatroom"]');
             const label_name = modal.querySelector('label[for="chatroom"]');
@@ -40,6 +43,7 @@ describe('The "Groupchats" Add modal', function () {
             nick_input.value = 'max';
             nick_input.value = 'max';
 
 
             modal.querySelector('form input[type="submit"]').click();
             modal.querySelector('form input[type="submit"]').click();
+            await mock.getRoomFeatures(_converse, 'lounge@muc.example.org');
             await u.waitUntil(() => _converse.chatboxes.length);
             await u.waitUntil(() => _converse.chatboxes.length);
             await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1);
             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);
             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 = modal.querySelector('input[name="nickname"]');
             nick_input.value = 'max';
             nick_input.value = 'max';
             modal.querySelector('form input[type="submit"]').click();
             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(() => _converse.chatboxes.models.filter(c => c.get('type') === 'chatroom').length === 2);
             await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).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(
             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"]');
             let nick_input = modal.querySelector('input[name="nickname"]');
             nick_input.value = 'max';
             nick_input.value = 'max';
             modal.querySelector('form input[type="submit"]').click();
             modal.querySelector('form input[type="submit"]').click();
+            await mock.getRoomFeatures(_converse, 'lounge@muc.example.org');
             await u.waitUntil(() => _converse.chatboxes.length);
             await u.waitUntil(() => _converse.chatboxes.length);
             await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1);
             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);
             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 = modal.querySelector('input[name="nickname"]');
             nick_input.value = 'max';
             nick_input.value = 'max';
             modal.querySelector('form input[type="submit"]').click();
             modal.querySelector('form input[type="submit"]').click();
+            await mock.getRoomFeatures(_converse, 'lounge-conference@muc.example.org');
             await u.waitUntil(
             await u.waitUntil(
                 () => _converse.chatboxes.models.filter(c => c.get('type') === 'chatroom').length === 2
                 () => _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, domain, [], [], ['muc.example.org'], 'items');
             await mock.waitUntilDiscoConfirmed(_converse, 'muc.example.org', [], [Strophe.NS.MUC]);
             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);
             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);
             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();
             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);
             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);
             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';
             nick_input.value = 'max';
 
 
             modal.querySelector('form input[type="submit"]').click();
             modal.querySelector('form input[type="submit"]').click();
-
             await u.waitUntil(() => name_input.classList.contains('error'));
             await u.waitUntil(() => name_input.classList.contains('error'));
             expect(name_input.classList.contains('is-invalid')).toBe(true);
             expect(name_input.classList.contains('is-invalid')).toBe(true);
             expect(modal.querySelector('.invalid-feedback')?.textContent).toBe('Groupchat id is invalid.');
             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());
             spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
 
 
             let jid = 'lounge@montague.lit';
             let jid = 'lounge@montague.lit';
+            const nick = 'romeo';
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
             const rosterview = document.querySelector('converse-roster');
             const rosterview = document.querySelector('converse-roster');
             await u.waitUntil(() => rosterview.querySelectorAll('.roster-group .group-toggle').length);
             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
             // 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();
             expect(room instanceof Model).toBeTruthy();
             let mucview = await u.waitUntil(() => _converse.chatboxviews.get(jid));
             let mucview = await u.waitUntil(() => _converse.chatboxviews.get(jid));
             expect(mucview.is_chatroom).toBeTruthy();
             expect(mucview.is_chatroom).toBeTruthy();
@@ -132,7 +137,10 @@ describe("Groupchats", function () {
 
 
             // Test with mixed case in JID
             // Test with mixed case in JID
             jid = 'Leisure@montague.lit';
             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();
             expect(room instanceof Model).toBeTruthy();
             mucview = await u.waitUntil(() => _converse.chatboxviews.get(jid.toLowerCase()));
             mucview = await u.waitUntil(() => _converse.chatboxviews.get(jid.toLowerCase()));
             await u.waitUntil(() => u.isVisible(mucview));
             await u.waitUntil(() => u.isVisible(mucview));
@@ -151,8 +159,10 @@ describe("Groupchats", function () {
             mucview.close();
             mucview.close();
 
 
             api.settings.set('muc_instant_rooms', false);
             api.settings.set('muc_instant_rooms', false);
+
             // Test with configuration
             // 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',
                 'nick': 'some1',
                 'auto_configure': true,
                 'auto_configure': true,
                 'roomconfig': {
                 'roomconfig': {
@@ -165,6 +175,8 @@ describe("Groupchats", function () {
                     'whois': 'anyone'
                     'whois': 'anyone'
                 }
                 }
             });
             });
+            await mock.getRoomFeatures(_converse, jid);
+            room = await promise;
             expect(room instanceof Model).toBeTruthy();
             expect(room instanceof Model).toBeTruthy();
 
 
             const IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
             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';
                     const muc_jid = 'coven@chat.shakespeare.lit';
                     await mock.waitForRoster(_converse, 'current', 0);
                     await mock.waitForRoster(_converse, 'current', 0);
                     await mock.openControlBox(_converse);
                     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 selector = `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`;
                     const features_query = await u.waitUntil(() =>
                     const features_query = await u.waitUntil(() =>
@@ -82,7 +82,7 @@ describe('Groupchats', () => {
                         </iq>`;
                         </iq>`;
                     _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
                     _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(
                     await u.waitUntil(
                         () => view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING
                         () => view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING
                     );
                     );
@@ -104,7 +104,7 @@ describe('Groupchats', () => {
 
 
                     const initials_el = avatar_el.querySelector('.avatar-initials');
                     const initials_el = avatar_el.querySelector('.avatar-initials');
                     expect(initials_el.textContent).toBe('AC');
                     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();
                     avatar_el.click();
 
 
                     const modal = _converse.api.modal.get('converse-muc-details-modal');
                     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');
             expect(rooms[10].textContent.trim()).toBe('A street');
 
 
             rooms[4].querySelector('.open-room').click();
             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);
             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
             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');
             const view = _converse.chatboxviews.get('inverness@chat.shakespeare.lit');
             expect(view.querySelector('.chatbox-title__text').textContent.trim()).toBe("Macbeth's Castle");
             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'},
                 view_mode: 'overlayed'},
             async function (_converse) {
             async function (_converse) {
 
 
-        const { api } = _converse;
-
         expect(_converse.session.get('rai_enabled_domains')).toBe(undefined);
         expect(_converse.session.get('rai_enabled_domains')).toBe(undefined);
 
 
-        const muc_jid = 'lounge@montague.lit';
+        const { api } = _converse;
         const nick = 'romeo';
         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.getRoomFeatures(_converse, muc_jid, []);
         await mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
         await mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
         await muc_creation_promise;
         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 () {
 describe("Groupchats", function () {
 
 
+    beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
+
     describe("An instant groupchat", function () {
     describe("An instant groupchat", function () {
 
 
         it("will be created when muc_instant_rooms is set to true",
         it("will be created when muc_instant_rooms is set to true",
                 mock.initConverse(['chatBoxesFetched'], { vcard: { nickname: '' } }, async function (_converse) {
                 mock.initConverse(['chatBoxesFetched'], { vcard: { nickname: '' } }, async function (_converse) {
 
 
             let IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
             let IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
+
+            const { api } = _converse;
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
             const nick = 'nicky';
             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 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());
             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.
             // We pretend this is a new room, so no disco info is returned.
             const features_stanza =
             const features_stanza =
-                stx`<iq from="lounge@montague.lit"
+                stx`<iq from="${muc_jid}"
                         id="${stanza.getAttribute('id')}"
                         id="${stanza.getAttribute('id')}"
                         to="romeo@montague.lit/desktop"
                         to="romeo@montague.lit/desktop"
                         type="error"
                         type="error"
@@ -30,19 +47,8 @@ describe("Groupchats", function () {
                 </iq>`;
                 </iq>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
             _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 = [];
             _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 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)
             // The user has just entered the room (because join was called)
             // and receives their own presence from the server.
             // and receives their own presence from the server.
@@ -97,8 +103,15 @@ describe("Groupchats", function () {
                     async function (_converse) {
                     async function (_converse) {
 
 
             const { api } = _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');
             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);
             expect(room.get('hidden')).toBe(false);
         }));
         }));
 
 
@@ -118,7 +131,8 @@ describe("Groupchats", function () {
             api.rooms.open(muc_jid);
             api.rooms.open(muc_jid);
             await mock.getRoomFeatures(_converse, muc_jid);
             await mock.getRoomFeatures(_converse, muc_jid);
             await mock.waitForReservedNick(_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;
             await view.model.messages.fetched;
 
 
             view.model.messages.create({
             view.model.messages.create({
@@ -510,6 +524,9 @@ describe("Groupchats", function () {
             await view.model.handleMessageStanza(msg);
             await view.model.handleMessageStanza(msg);
             await u.waitUntil(()  => view.querySelector('.chat-msg__text a'));
             await u.waitUntil(()  => view.querySelector('.chat-msg__text a'));
             view.querySelector('.chat-msg__text a').click();
             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)
             await u.waitUntil(() => _converse.chatboxes.length === 3)
             expect(_converse.chatboxes.pluck('id').includes('coven@chat.shakespeare.lit')).toBe(true);
             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 muc_jid = 'coven@chat.shakespeare.lit';
             const nick = 'romeo';
             const nick = 'romeo';
-            await _converse.api.rooms.open(muc_jid);
+            _converse.api.rooms.open(muc_jid);
             await mock.getRoomFeatures(_converse, muc_jid);
             await mock.getRoomFeatures(_converse, muc_jid);
             await mock.waitForReservedNick(_converse, muc_jid, nick);
             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 =
             const presence =
                 stx`<presence to="romeo@montague.lit/orchard"
                 stx`<presence to="romeo@montague.lit/orchard"
                         from="coven@chat.shakespeare.lit/some1"
                         from="coven@chat.shakespeare.lit/some1"
@@ -553,12 +570,11 @@ describe("Groupchats", function () {
 
 
             const muc_jid = 'coven@chat.shakespeare.lit';
             const muc_jid = 'coven@chat.shakespeare.lit';
             const nick = 'some1';
             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);
             await mock.getRoomFeatures(_converse, muc_jid);
             const sent_stanzas = _converse.api.connection.get().sent_stanzas;
             const sent_stanzas = _converse.api.connection.get().sent_stanzas;
             await u.waitUntil(() => sent_stanzas.filter(iq => sizzle('presence history', iq).length));
             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');
             await _converse.api.waitUntil('chatRoomViewInitialized');
 
 
             /* We don't show join/leave messages for existing occupants. We
             /* We don't show join/leave messages for existing occupants. We
@@ -596,6 +612,7 @@ describe("Groupchats", function () {
                 .c('status', {code: '110'});
                 .c('status', {code: '110'});
             _converse.api.connection.get()._dataRecv(mock.createRequest(presence));
             _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);
             const csntext = await u.waitUntil(() => view.querySelector('.chat-content__notifications')?.textContent);
             expect(csntext.trim()).toEqual("some1 has entered the groupchat");
             expect(csntext.trim()).toEqual("some1 has entered the groupchat");
 
 
@@ -1055,26 +1072,15 @@ describe("Groupchats", function () {
                 IQ_id = sendIQ.call(this, iq, callback, errback);
                 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';
             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 =
             const presence =
-                stx`<presence to='romeo@montague.lit/_converse.js-29092160'
+                stx`<presence to='${own_jid}'
                         from='coven@chat.shakespeare.lit/some1'
                         from='coven@chat.shakespeare.lit/some1'
                         xmlns="jabber:client">
                         xmlns="jabber:client">
                     <x xmlns='${Strophe.NS.MUC_USER}'>
                     <x xmlns='${Strophe.NS.MUC_USER}'>
@@ -1085,9 +1091,15 @@ describe("Groupchats", function () {
                 </presence>`;
                 </presence>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(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');
             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 sel = 'iq query[xmlns="http://jabber.org/protocol/muc#owner"]';
             const iq = await u.waitUntil(() => sent_IQs.filter(iq => sizzle(sel, iq).length).pop());
             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",
         it("properly handles notification that a room has been destroyed",
                 mock.initConverse([], {}, async function (_converse) {
                 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 =
             const presence =
                 stx`<presence from="problematic@muc.montague.lit"
                 stx`<presence from="problematic@muc.montague.lit"
                         id="n13mt3l"
                         id="n13mt3l"
@@ -1504,7 +1520,7 @@ describe("Groupchats", function () {
                     </error>
                     </error>
                 </presence>`;
                 </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));
             _converse.api.connection.get()._dataRecv(mock.createRequest(presence));
             const msg = await u.waitUntil(() => view.querySelector('.chatroom-body .disconnect-msg'));
             const msg = await u.waitUntil(() => view.querySelector('.chatroom-body .disconnect-msg'));
             expect(msg.textContent.trim()).toBe('This groupchat no longer exists');
             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) {
                 mock.initConverse([], {}, async function (_converse) {
 
 
             await mock.waitForRoster(_converse, 'current'); // We need roster contacts, who can invite us
             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 name = mock.cur_names[0];
             const from_jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const from_jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
             await u.waitUntil(() => _converse.roster.get(from_jid).vcard.get('fullname'));
             await u.waitUntil(() => _converse.roster.get(from_jid).vcard.get('fullname'));
 
 
             spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
             spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
-            await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
-            const view = _converse.chatboxviews.get('lounge@montague.lit');
-            await view.close(); // Hack, otherwise we have to mock stanzas.
-
-            const muc_jid = 'lounge@montague.lit';
-            const reason = "Please join this groupchat";
-
             expect(_converse.chatboxes.models.length).toBe(1);
             expect(_converse.chatboxes.models.length).toBe(1);
             expect(_converse.chatboxes.models[0].id).toBe("controlbox");
             expect(_converse.chatboxes.models[0].id).toBe("controlbox");
 
 
+            const reason = "Please join this groupchat";
             const stanza = stx`
             const stanza = stx`
                 <message xmlns="jabber:client"
                 <message xmlns="jabber:client"
                         to="${_converse.bare_jid}"
                         to="${_converse.bare_jid}"
@@ -1617,7 +1628,11 @@ describe("Groupchats", function () {
                         id="9bceb415-f34b-4fa4-80d5-c0d076a24231">
                         id="9bceb415-f34b-4fa4-80d5-c0d076a24231">
                    <x xmlns="jabber:x:conference" jid="${muc_jid}" reason="${reason}"/>
                    <x xmlns="jabber:x:conference" jid="${muc_jid}" reason="${reason}"/>
                 </message>`.tree();
                 </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(
             expect(_converse.api.confirm).toHaveBeenCalledWith(
                 name + ' has invited you to join a groupchat: '+ muc_jid +
                 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 IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
             const muc_jid = 'coven@chat.shakespeare.lit';
             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(
             const stanza = await u.waitUntil(() => IQ_stanzas.filter(
                 iq => iq.querySelector(
                 iq => iq.querySelector(
                     `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
                     `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",
         it("can be saved to, and retrieved from, browserStorage",
                 mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
                 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
             // We instantiate a new ChatBoxes collection, which by default
             // will be empty.
             // will be empty.
             await mock.openControlBox(_converse);
             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'",
         it("can be closed again by clicking a DOM element with class 'close-chatbox-button'",
                 mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
                 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(model, 'close').and.callThrough();
             spyOn(_converse.api, "trigger").and.callThrough();
             spyOn(_converse.api, "trigger").and.callThrough();
             spyOn(model, 'leave');
             spyOn(model, 'leave');
@@ -2287,9 +2306,10 @@ describe("Groupchats", function () {
         it("will show an error message if the groupchat requires a password",
         it("will show an error message if the groupchat requires a password",
                 mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
                 mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
 
 
+            const { api } = _converse;
             const muc_jid = 'protected@montague.lit';
             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 =
             const presence =
                     stx`<presence from="${muc_jid}/romeo"
                     stx`<presence from="${muc_jid}/romeo"
@@ -2302,9 +2322,9 @@ describe("Groupchats", function () {
                             <not-authorized xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
                             <not-authorized xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
                         </error>
                         </error>
                     </presence>`;
                     </presence>`;
-
             _converse.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 chat_body = view.querySelector('.chatroom-body');
             const chat_body = view.querySelector('.chatroom-body');
             await u.waitUntil(() => chat_body.querySelectorAll('form.chatroom-form').length === 1);
             await u.waitUntil(() => chat_body.querySelectorAll('form.chatroom-form').length === 1);
             expect(chat_body.querySelector('.chatroom-form label').textContent.trim())
             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",
         it("will show an error message if the groupchat is members-only and the user not included",
                 mock.initConverse([], {}, async function (_converse) {
                 mock.initConverse([], {}, async function (_converse) {
 
 
+            const { api } = _converse;
             const muc_jid = 'members-only@muc.montague.lit'
             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(
             const iq = await u.waitUntil(() => _converse.api.connection.get().IQ_stanzas.filter(
                 iq => iq.querySelector(
                 iq => iq.querySelector(
                     `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
                     `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
                 )).pop());
                 )).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
             // State that the chat is members-only via the features IQ
             const features_stanza =
             const features_stanza =
                 stx`<iq from="${muc_jid}"
                 stx`<iq from="${muc_jid}"
@@ -2345,6 +2371,8 @@ describe("Groupchats", function () {
                     </query>
                     </query>
                 </iq>`;
                 </iq>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
             _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);
             await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING);
 
 
             const presence =
             const presence =
@@ -2367,8 +2395,9 @@ describe("Groupchats", function () {
         it("will show an error message if the user has been banned",
         it("will show an error message if the user has been banned",
                 mock.initConverse([], {}, async function (_converse) {
                 mock.initConverse([], {}, async function (_converse) {
 
 
+            const { api } = _converse;
             const muc_jid = 'off-limits@muc.montague.lit'
             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(
             const iq = await u.waitUntil(() => _converse.api.connection.get().IQ_stanzas.filter(
                 iq => iq.querySelector(
                 iq => iq.querySelector(
@@ -2390,7 +2419,7 @@ describe("Groupchats", function () {
                     </iq>`
                     </iq>`
             _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
             _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);
             await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING);
 
 
             const presence =
             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",
         it("will show an error message if the user is not allowed to have created the groupchat",
                 mock.initConverse([], {}, async function (_converse) {
                 mock.initConverse([], {}, async function (_converse) {
 
 
+            const { api } = _converse;
             const muc_jid = 'impermissable@muc.montague.lit'
             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.
             // 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(
             const iq = await u.waitUntil(() => _converse.api.connection.get().IQ_stanzas.filter(
@@ -2425,7 +2455,7 @@ describe("Groupchats", function () {
                 )).pop());
                 )).pop());
 
 
             const features_stanza =
             const features_stanza =
-                stx`<iq from="room@conference.example.org"
+                stx`<iq from="${muc_jid}"
                         id="${iq.getAttribute('id')}"
                         id="${iq.getAttribute('id')}"
                         to="romeo@montague.lit/desktop"
                         to="romeo@montague.lit/desktop"
                         type="error"
                         type="error"
@@ -2436,8 +2466,6 @@ describe("Groupchats", function () {
                 </iq>`;
                 </iq>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
             _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 =
             const presence =
                 stx`<presence xmlns="jabber:client"
                 stx`<presence xmlns="jabber:client"
@@ -2451,6 +2479,12 @@ describe("Groupchats", function () {
                     </error>
                     </error>
                 </presence>`;
                 </presence>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(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'));
             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.');
             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",
         it("will show an error message if the groupchat doesn't yet exist",
                 mock.initConverse([], {}, async function (_converse) {
                 mock.initConverse([], {}, async function (_converse) {
 
 
+            const { api } = _converse;
             const muc_jid = 'nonexistent@muc.montague.lit'
             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(
             const iq = await u.waitUntil(() => _converse.api.connection.get().IQ_stanzas.filter(
                 iq => iq.querySelector(
                 iq => iq.querySelector(
@@ -2479,7 +2514,7 @@ describe("Groupchats", function () {
                 </iq>`;
                 </iq>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
             _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));
             await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING));
 
 
             const presence =
             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",
         it("will show an error message if the groupchat has reached its maximum number of participants",
                 mock.initConverse([], {}, async function (_converse) {
                 mock.initConverse([], {}, async function (_converse) {
 
 
+            const { api } = _converse;
             const muc_jid = 'maxed-out@muc.montague.lit'
             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(
             const iq = await u.waitUntil(() => _converse.api.connection.get().IQ_stanzas.filter(
                 iq => iq.querySelector(
                 iq => iq.querySelector(
@@ -2523,7 +2559,7 @@ describe("Groupchats", function () {
                 </iq>`;
                 </iq>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
             _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));
             await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING));
 
 
             const presence =
             const presence =
@@ -2547,7 +2583,10 @@ describe("Groupchats", function () {
     describe("The affiliations delta", function () {
     describe("The affiliations delta", function () {
 
 
         it("can be computed in various ways", mock.initConverse([], {}, async function (_converse) {
         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 exclude_existing = false;
             let remove_absentees = false;
             let remove_absentees = false;
             let new_list = [];
             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);
         expect(model.occupants.at(0).get('nick')).toBe(nick);
 
 
         const view = _converse.chatboxviews.get(muc_jid);
         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();
         dropdown_item.click();
 
 
         const modal = _converse.api.modal.get('converse-muc-nickname-modal');
         const modal = _converse.api.modal.get('converse-muc-nickname-modal');
@@ -35,7 +35,6 @@ describe("A MUC", function () {
         modal.querySelector('input[type="submit"]')?.click();
         modal.querySelector('input[type="submit"]')?.click();
 
 
         await u.waitUntil(() => !u.isVisible(modal));
         await u.waitUntil(() => !u.isVisible(modal));
-
         const { sent_stanzas } = _converse.api.connection.get();
         const { sent_stanzas } = _converse.api.connection.get();
         const sent_stanza = sent_stanzas.pop()
         const sent_stanza = sent_stanzas.pop()
         expect(sent_stanza).toEqualStanza(
         expect(sent_stanza).toEqualStanza(
@@ -52,7 +51,7 @@ describe("A MUC", function () {
         _converse.api.connection.get()._dataRecv(mock.createRequest(
         _converse.api.connection.get()._dataRecv(mock.createRequest(
             stx`
             stx`
             <presence
             <presence
-                xmlns="jabber:server"
+                xmlns="jabber:client"
                 from='${muc_jid}/${nick}'
                 from='${muc_jid}/${nick}'
                 id='DC352437-C019-40EC-B590-AF29E879AF98'
                 id='DC352437-C019-40EC-B590-AF29E879AF98'
                 to='${_converse.jid}'
                 to='${_converse.jid}'
@@ -74,7 +73,7 @@ describe("A MUC", function () {
         _converse.api.connection.get()._dataRecv(mock.createRequest(
         _converse.api.connection.get()._dataRecv(mock.createRequest(
             stx`
             stx`
             <presence
             <presence
-                xmlns="jabber:server"
+                xmlns="jabber:client"
                 from='${muc_jid}/${newnick}'
                 from='${muc_jid}/${newnick}'
                 id='5B4F27A4-25ED-43F7-A699-382C6B4AFC67'
                 id='5B4F27A4-25ED-43F7-A699-382C6B4AFC67'
                 to='${_converse.jid}'>
                 to='${_converse.jid}'>
@@ -247,7 +246,7 @@ describe("A MUC", function () {
 
 
             const IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
             const IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
             const muc_jid = 'lounge@montague.lit';
             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(
             let stanza = await u.waitUntil(() => IQ_stanzas.filter(
                 iq => iq.querySelector(
                 iq => iq.querySelector(
@@ -267,29 +266,19 @@ describe("A MUC", function () {
                 </iq>`;
                 </iq>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
             _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(
             const iq = await u.waitUntil(() => IQ_stanzas.filter(
                     s => sizzle(`iq[to="${muc_jid}"] query[node="x-roomuser-item"]`, s).length
                     s => sizzle(`iq[to="${muc_jid}"] query[node="x-roomuser-item"]`, s).length
                 ).pop());
                 ).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`
             stanza = stx`
                 <iq type="result"
                 <iq type="result"
                     id="${iq.getAttribute("id")}"
                     id="${iq.getAttribute("id")}"
-                    from="${view.model.get("jid")}"
+                    from="${muc_jid}"
                     to="${_converse.api.connection.get().jid}"
                     to="${_converse.api.connection.get().jid}"
                     xmlns="jabber:client">
                     xmlns="jabber:client">
                     <query xmlns="http://jabber.org/protocol/disco#info" node="x-roomuser-item">
                     <query xmlns="http://jabber.org/protocol/disco#info" node="x-roomuser-item">
@@ -316,9 +305,15 @@ describe("A MUC", function () {
                         <status code="210"/>
                         <status code="210"/>
                     </x>
                     </x>
                 </presence>`;
                 </presence>`;
-
             _converse.api.connection.get()._dataRecv(mock.createRequest(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 u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED));
             await mock.returnMemberLists(_converse, muc_jid, [], ['member', 'admin', 'owner']);
             await mock.returnMemberLists(_converse, muc_jid, [], ['member', 'admin', 'owner']);
             await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-info').length);
             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",
         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) {
                 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');
             expect(view.model.get('nick')).toBe('Benedict-Cucumberpatch');
         }));
         }));
 
 
@@ -340,30 +339,9 @@ describe("A MUC", function () {
 
 
             const muc_jid = 'conflicted@muc.montague.lit';
             const muc_jid = 'conflicted@muc.montague.lit';
             await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
             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
                 <presence
                         from="${muc_jid}/romeo"
                         from="${muc_jid}/romeo"
                         id="${u.getUniqueId()}"
                         id="${u.getUniqueId()}"
@@ -374,53 +352,64 @@ describe("A MUC", function () {
                     <error by="${muc_jid}" type="cancel">
                     <error by="${muc_jid}" type="cancel">
                         <conflict xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
                         <conflict xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
                     </error>
                     </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'));
             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.');
             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",
         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) {
                 mock.initConverse(['chatBoxesFetched'], {vcard: { nickname: '' }}, async function (_converse) {
 
 
             const { api } = _converse;
             const { api } = _converse;
             const muc_jid = 'conflicting@muc.montague.lit'
             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.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`
             let presence = stx`
-                <presence
-                        xmlns="jabber:client"
+                <presence xmlns="jabber:client"
                         from='${muc_jid}/romeo'
                         from='${muc_jid}/romeo'
                         id='${u.getUniqueId()}'
                         id='${u.getUniqueId()}'
-                        to='romeo@montague.lit/pda'
+                        to='${api.connection.get().jid}'
                         type='error'>
                         type='error'>
                     <x xmlns='http://jabber.org/protocol/muc'/>
                     <x xmlns='http://jabber.org/protocol/muc'/>
                     <error by='${muc_jid}' type='cancel'>
                     <error by='${muc_jid}' type='cancel'>
                         <conflict xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
                         <conflict xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
                     </error>
                     </error>
                 </presence>`;
                 </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 = stx`
                 <presence
                 <presence
@@ -436,7 +425,23 @@ describe("A MUC", function () {
                 </presence>`;
                 </presence>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(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 = stx`
                 <presence
                 <presence
@@ -451,34 +456,30 @@ describe("A MUC", function () {
                     </error>
                     </error>
                 </presence>`;
                 </presence>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(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",
         it("will show an error message if the user's nickname doesn't conform to groupchat policy",
                 mock.initConverse([], {}, async function (_converse) {
                 mock.initConverse([], {}, async function (_converse) {
 
 
+            const { api } = _converse;
             const muc_jid = 'conformist@muc.montague.lit'
             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`
             const presence = stx`
                 <presence
                 <presence
@@ -492,8 +493,9 @@ describe("A MUC", function () {
                         <not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
                         <not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
                     </error>
                     </error>
                 </presence>`;
                 </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'));
             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.");
             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: '' },
                 vcard: { nickname: '' },
             }, async function (_converse) {
             }, async function (_converse) {
 
 
+            const muc_jid = 'lounge@montague.lit';
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
             await mock.waitForRoster(_converse, 'current', 0);
             await mock.waitForRoster(_converse, 'current', 0);
             const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
             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');
             const modal = _converse.api.modal.get('converse-add-muc-modal');
             await u.waitUntil(() => u.isVisible(modal), 1000)
             await u.waitUntil(() => u.isVisible(modal), 1000)
             const name_input = modal.querySelector('input[name="chatroom"]');
             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('label[for="nickname"]')).toBe(null);
             expect(modal.querySelector('input[name="nickname"]')).toBe(null);
             expect(modal.querySelector('input[name="nickname"]')).toBe(null);
             modal.querySelector('form input[type="submit"]').click();
             modal.querySelector('form input[type="submit"]').click();
+
+            await mock.getRoomFeatures(_converse, muc_jid);
             await u.waitUntil(() => _converse.chatboxes.length > 1);
             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');
             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 nick = 'romeo';
         const sent_stanzas = _converse.api.connection.get().sent_stanzas;
         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.getRoomFeatures(_converse, muc_jid, []);
         await mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
         await mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
         await muc_creation_promise;
         await muc_creation_promise;

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

@@ -2,7 +2,6 @@
 
 
 const { u } = converse.env;
 const { u } = converse.env;
 
 
-
 describe("The list of MUC domains", function () {
 describe("The list of MUC domains", function () {
     it("is shown in controlbox", mock.initConverse(
     it("is shown in controlbox", mock.initConverse(
             ['chatBoxesFetched'],
             ['chatBoxesFetched'],
@@ -16,7 +15,10 @@ describe("The list of MUC domains", function () {
         const controlbox = _converse.chatboxviews.get('controlbox');
         const controlbox = _converse.chatboxviews.get('controlbox');
         let list = controlbox.querySelector('.list-container--openrooms');
         let list = controlbox.querySelector('.list-container--openrooms');
         expect(u.hasClass('hidden', list)).toBeTruthy();
         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');
         const lview = controlbox.querySelector('converse-rooms-list');
         // Check that the group is shown
         // 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);
         await u.waitUntil(() => lview.querySelectorAll(".open-room").length);
         let room_els = lview.querySelectorAll(".open-room");
         let room_els = lview.querySelectorAll(".open-room");
         expect(room_els.length).toBe(1);
         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
         // Check that a second room in the same domain is shown in the same
         // domain group.
         // 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);
         await u.waitUntil(() => lview.querySelectorAll(".open-room").length > 1);
         group_els = lview.querySelectorAll(".muc-domain-group");
         group_els = lview.querySelectorAll(".muc-domain-group");
         expect(group_els.length).toBe(1); // still only one 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");
         room_els = lview.querySelectorAll(".open-room");
         expect(room_els.length).toBe(2); // but two rooms inside it
         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);
         await u.waitUntil(() => lview.querySelectorAll(".open-room").length > 2);
         room_els = lview.querySelectorAll(".open-room");
         room_els = lview.querySelectorAll(".open-room");
         expect(room_els.length).toBe(3);
         expect(room_els.length).toBe(3);
@@ -64,7 +71,7 @@ describe("The list of MUC domains", function () {
         expect(room_els.length).toBe(1);
         expect(room_els.length).toBe(1);
         group_els = lview.querySelectorAll(".muc-domain-group");
         group_els = lview.querySelectorAll(".muc-domain-group");
         expect(group_els.length).toBe(1);
         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');
         expect(group_els[0].children[0].innerText.trim()).toBe('montague.lit');
         list = controlbox.querySelector('.list-container--openrooms');
         list = controlbox.querySelector('.list-container--openrooms');
         u.waitUntil(() => Array.from(list.classList).includes('hidden'));
         u.waitUntil(() => Array.from(list.classList).includes('hidden'));
@@ -93,7 +100,10 @@ describe("A MUC domain group", function () {
         await mock.openControlBox(_converse);
         await mock.openControlBox(_converse);
         const controlbox = _converse.chatboxviews.get('controlbox');
         const controlbox = _converse.chatboxviews.get('controlbox');
         const list = controlbox.querySelector('.list-container--openrooms');
         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');
         const lview = controlbox.querySelector('converse-rooms-list');
         await u.waitUntil(() => lview.querySelectorAll(".muc-domain-group").length);
         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.
                                         // have to mock stanza traffic.
             }, async function (_converse) {
             }, async function (_converse) {
 
 
+        const { api } = _converse;
         await mock.waitForRoster(_converse, 'current', 0);
         await mock.waitForRoster(_converse, 'current', 0);
         await mock.openControlBox(_converse);
         await mock.openControlBox(_converse);
         const controlbox = _converse.chatboxviews.get('controlbox');
         const controlbox = _converse.chatboxviews.get('controlbox');
         let list = controlbox.querySelector('.list-container--openrooms');
         let list = controlbox.querySelector('.list-container--openrooms');
         expect(u.hasClass('hidden', list)).toBeTruthy();
         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');
         const lview = controlbox.querySelector('converse-rooms-list');
         await u.waitUntil(() => lview.querySelectorAll(".open-room").length);
         await u.waitUntil(() => lview.querySelectorAll(".open-room").length);
         let room_els = lview.querySelectorAll(".open-room");
         let room_els = lview.querySelectorAll(".open-room");
         expect(room_els.length).toBe(1);
         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);
         await u.waitUntil(() => lview.querySelectorAll(".open-room").length > 1);
         room_els = lview.querySelectorAll(".open-room");
         room_els = lview.querySelectorAll(".open-room");
         expect(room_els.length).toBe(2);
         expect(room_els.length).toBe(2);
@@ -35,7 +42,7 @@ describe("A list of open groupchats", function () {
         await view.close();
         await view.close();
         room_els = lview.querySelectorAll(".open-room");
         room_els = lview.querySelectorAll(".open-room");
         expect(room_els.length).toBe(1);
         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');
         list = controlbox.querySelector('.list-container--openrooms');
         u.waitUntil(() => Array.from(list.classList).includes('hidden'));
         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 controlbox = _converse.chatboxviews.get('controlbox');
         const u = converse.env.utils;
         const u = converse.env.utils;
         const muc_jid = 'coven@chat.shakespeare.lit';
         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');
         const lview = controlbox.querySelector('converse-rooms-list');
         await u.waitUntil(() => lview.querySelectorAll(".open-room").length);
         await u.waitUntil(() => lview.querySelectorAll(".open-room").length);
         let room_els = lview.querySelectorAll(".available-chatroom");
         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];
         let item = room_els[0];
         await u.waitUntil(() => _converse.chatboxes.get(muc_jid).get('hidden') === false);
         await u.waitUntil(() => _converse.chatboxes.get(muc_jid).get('hidden') === false);
         await u.waitUntil(() => u.hasClass('open', item), 1000);
         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);
         await u.waitUntil(() => lview.querySelectorAll(".open-room").length > 1);
         room_els = lview.querySelectorAll(".open-room");
         room_els = lview.querySelectorAll(".open-room");
         expect(room_els.length).toBe(2);
         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");
         room_els = lview.querySelectorAll(".available-chatroom.open");
         expect(room_els.length).toBe(1);
         expect(room_els.length).toBe(1);
         item = room_els[0];
         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(
     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));
         spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
         expect(_converse.chatboxes.length).toBe(1);
         expect(_converse.chatboxes.length).toBe(1);
         await mock.waitForRoster(_converse, 'current', 0);
         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);
         await mock.openControlBox(_converse);
         const controlbox = _converse.chatboxviews.get('controlbox');
         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");
         const close_el = rooms_list.querySelector(".close-room");
         close_el.click();
         close_el.click();
         expect(_converse.api.confirm).toHaveBeenCalledWith(
         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);
         await u.waitUntil(() => rooms_list.querySelectorAll(".open-room").length === 0);
         expect(_converse.chatboxes.length).toBe(1);
         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);
     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);
     await openControlBox(_converse);
+
     document.querySelector('converse-rooms-list .show-add-muc-modal').click();
     document.querySelector('converse-rooms-list .show-add-muc-modal').click();
     closeControlBox(_converse);
     closeControlBox(_converse);
     const modal = _converse.api.modal.get('converse-add-muc-modal');
     const modal = _converse.api.modal.get('converse-add-muc-modal');
     await u.waitUntil(() => u.isVisible(modal), 1500)
     await u.waitUntil(() => u.isVisible(modal), 1500)
-    modal.querySelector('input[name="chatroom"]').value = jid;
+    modal.querySelector('input[name="chatroom"]').value = muc_jid;
     if (nick) {
     if (nick) {
         modal.querySelector('input[name="nickname"]').value = nick;
         modal.querySelector('input[name="nickname"]').value = nick;
     }
     }
     modal.querySelector('form input[type="submit"]').click();
     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={}) {
 async function getRoomFeatures (_converse, muc_jid, features=[], settings={}) {
@@ -409,6 +428,7 @@ async function openAndEnterChatRoom (
     ) {
     ) {
     const { api } = _converse;
     const { api } = _converse;
     muc_jid = muc_jid.toLowerCase();
     muc_jid = muc_jid.toLowerCase();
+
     const room_creation_promise = api.rooms.open(muc_jid, settings, force_open);
     const room_creation_promise = api.rooms.open(muc_jid, settings, force_open);
     await getRoomFeatures(_converse, muc_jid, features, settings);
     await getRoomFeatures(_converse, muc_jid, features, settings);
     await waitForReservedNick(_converse, muc_jid, nick);
     await waitForReservedNick(_converse, muc_jid, nick);
@@ -890,7 +910,6 @@ Object.assign(mock, {
     openAndEnterChatRoom,
     openAndEnterChatRoom,
     openChatBoxFor,
     openChatBoxFor,
     openChatBoxes,
     openChatBoxes,
-    openChatRoom,
     openChatRoomViaModal,
     openChatRoomViaModal,
     openControlBox,
     openControlBox,
     ownDeviceHasBeenPublished,
     ownDeviceHasBeenPublished,
@@ -903,6 +922,7 @@ Object.assign(mock, {
     view_mode,
     view_mode,
     waitForReservedNick,
     waitForReservedNick,
     waitForRoster,
     waitForRoster,
+    waitOnDiscoInfoForNewMUC,
     waitUntilBlocklistInitialized,
     waitUntilBlocklistInitialized,
     waitUntilBookmarksReturned,
     waitUntilBookmarksReturned,
     waitUntilDiscoConfirmed
     waitUntilDiscoConfirmed

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

@@ -18,6 +18,10 @@ export default class MUCHeading extends CustomElement {
      * @param {Event} ev
      * @param {Event} ev
      */
      */
     showRoomDetailsModal(ev: Event): void;
     showRoomDetailsModal(ev: Event): void;
+    /**
+     * @param {Event} ev
+     */
+    showNicknameModal(ev: Event): void;
     /**
     /**
      * @param {Event} ev
      * @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;
     jid: any;
-    connectedCallback(): void;
     model: any;
     model: any;
+    /**
+     * @param {Map<string, any>} changed
+     */
+    shouldUpdate(changed: Map<string, any>): boolean;
     render(): import("lit").TemplateResult<1>;
     render(): import("lit").TemplateResult<1>;
-    submitNickname(ev: any): void;
+    /**
+     * @param {Event} ev
+     */
+    submitNickname(ev: Event): void;
     closeModal(): void;
     closeModal(): void;
 }
 }
 import { CustomElement } from 'shared/components/element';
 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;
 export default _default;
 //# sourceMappingURL=muc-nickname-form.d.ts.map
 //# sourceMappingURL=muc-nickname-form.d.ts.map