Browse Source

Bugfix. Send out stanza when updating a contact

JC Brand 4 months ago
parent
commit
52f4900816

+ 1 - 1
karma.conf.js

@@ -45,7 +45,7 @@ module.exports = function(config) {
       { pattern: "src/headless/shared/settings/tests/settings.js", type: 'module' },
       { pattern: "src/headless/tests/converse.js", type: 'module' },
       { pattern: "src/headless/tests/eventemitter.js", type: 'module' },
-      { pattern: "src/modals/tests/user-details-modal.js", type: 'module' },
+      { pattern: "src/shared/modals/tests/user-details-modal.js", type: 'module' },
       { pattern: "src/plugins/adhoc-views/tests/adhoc.js", type: 'module' },
       { pattern: "src/plugins/bookmark-views/tests/bookmarks-list.js", type: 'module' },
       { pattern: "src/plugins/bookmark-views/tests/bookmarks.js", type: 'module' },

+ 24 - 6
src/headless/plugins/roster/contact.js

@@ -166,16 +166,34 @@ class RosterContact extends ModelWithVCard(ColorAwareModel(Model)) {
         return promise;
     }
 
+    /**
+     * @param {import('./types').RosterContactUpdateAttrs} attrs
+     * @returns {Promise}
+     */
+    async update (attrs) {
+        this.save(attrs);
+        return await api.sendIQ(
+            stx`<iq xmlns="jabber:client" type="set">
+                <query xmlns="${Strophe.NS.ROSTER}">
+                    <item jid="${this.get("jid")}" name="${this.get("nickname")}">
+                        ${this.get("groups")?.map(/** @param {string} group */ (group) => stx`<group>${group}</group>`)}
+                    </item>
+                </query>
+            </iq>`
+        );
+    }
+
     /**
      * Instruct the XMPP server to remove this contact from our roster
-     * @async
      * @returns {Promise}
      */
-    sendRosterRemoveStanza () {
-        const iq = $iq({type: 'set'})
-            .c('query', {xmlns: Strophe.NS.ROSTER})
-            .c('item', {jid: this.get('jid'), subscription: "remove"});
-        return api.sendIQ(iq);
+    async sendRosterRemoveStanza () {
+        const iq = stx`<iq type="set" xmlns="jabber:client">
+            <query xmlns="${Strophe.NS.ROSTER}">
+                <item jid="${this.get('jid')}" subscription="remove"/>
+            </query>
+        </iq>`;
+        return await api.sendIQ(iq);
     }
 }
 

+ 1 - 1
src/headless/plugins/roster/contacts.js

@@ -328,7 +328,7 @@ class RosterContacts extends Collection {
     /**
      * Update or create RosterContact models based on the given `item` XML
      * node received in the resulting IQ stanza from the server.
-     * @param { Element } item
+     * @param {Element} item
      */
     updateContact (item) {
         const jid = item.getAttribute('jid');

+ 5 - 0
src/headless/plugins/roster/types.ts

@@ -1,3 +1,8 @@
+export type RosterContactUpdateAttrs = {
+    nickname?: string; // The name of that user
+    groups?: string[]; // Any roster groups the user might belong to
+}
+
 export type RosterContactAttributes = {
     jid: string;
     subscription: ('none'|'to'|'from'|'both');

+ 5 - 1
src/headless/types/plugins/roster/contact.d.ts

@@ -191,9 +191,13 @@ declare class RosterContact extends RosterContact_base {
      * @returns {Promise<Error|Element>}
      */
     remove(unauthorize?: boolean): Promise<Error | Element>;
+    /**
+     * @param {import('./types').RosterContactUpdateAttrs} attrs
+     * @returns {Promise}
+     */
+    update(attrs: import("./types").RosterContactUpdateAttrs): Promise<any>;
     /**
      * Instruct the XMPP server to remove this contact from our roster
-     * @async
      * @returns {Promise}
      */
     sendRosterRemoveStanza(): Promise<any>;

+ 1 - 1
src/headless/types/plugins/roster/contacts.d.ts

@@ -75,7 +75,7 @@ declare class RosterContacts extends Collection {
     /**
      * Update or create RosterContact models based on the given `item` XML
      * node received in the resulting IQ stanza from the server.
-     * @param { Element } item
+     * @param {Element} item
      */
     updateContact(item: Element): any;
     /**

+ 4 - 0
src/headless/types/plugins/roster/types.d.ts

@@ -1,3 +1,7 @@
+export type RosterContactUpdateAttrs = {
+    nickname?: string;
+    groups?: string[];
+};
 export type RosterContactAttributes = {
     jid: string;
     subscription: ('none' | 'to' | 'from' | 'both');

+ 2 - 0
src/shared/modals/templates/user-details.js

@@ -142,6 +142,8 @@ export function tplUserDetailsModal(el) {
     }
 
     const { contact } = el.model;
+    if (!contact) return ''; // Happens during tests
+
     const name = contact.get("nickname") || contact.vcard?.get('fullname');
     const groups = contact.get("groups");
 

+ 70 - 20
src/shared/modals/tests/user-details-modal.js

@@ -1,33 +1,78 @@
 /*global mock, converse */
-
-const u = converse.env.utils;
+const { sizzle, u } = converse.env;
 
 describe("The User Details Modal", function () {
+    beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
 
-    it("can be used to remove a contact",
+    it("can be used to set a contact's name and groups",
             mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
 
+        const { api } = _converse;
         await mock.waitForRoster(_converse, 'current', 1);
-        _converse.api.trigger('rosterContactsFetched');
+        api.trigger('rosterContactsFetched');
 
         const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
         await mock.openChatBoxFor(_converse, contact_jid);
         await u.waitUntil(() => _converse.chatboxes.length > 1);
 
         const view = _converse.chatboxviews.get(contact_jid);
-        let show_modal_button = view.querySelector('.show-user-details-modal');
+        let show_modal_button = view.querySelector('.show-msg-author-modal');
         show_modal_button.click();
-        const modal = _converse.api.modal.get('converse-user-details-modal');
-        await u.waitUntil(() => u.isVisible(modal), 1000);
-        spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
+        let modal = api.modal.get('converse-user-details-modal');
+        await u.waitUntil(() => u.isVisible(modal));
+        modal.querySelector('#edit-tab').click();
+
+        const name_input = await u.waitUntil(() => modal.querySelector('input[name="name"]'));
+        expect(name_input.value).toBe('Mercutio');
+
+        const groups_input = modal.querySelector('input[name="groups"]');
+        expect(groups_input.value).toBe('Colleagues,friends & acquaintences');
+
+        const sent_stanzas = _converse.api.connection.get().sent_stanzas;
+        while (sent_stanzas.length) sent_stanzas.pop();
+
+        name_input.value = 'New Name';
+        groups_input.value = 'Other';
+        modal.querySelector('button[type="submit"]').click();
+        await u.waitUntil(() => modal.getAttribute('aria-hidden'));
+
+        const sent_IQ = await u.waitUntil(() => sent_stanzas.pop());
+        expect(sent_IQ).toEqualStanza(stx`
+            <iq xmlns="jabber:client"
+                    type="set"
+                    id="${sent_IQ.getAttribute('id')}">
+                <query xmlns="jabber:iq:roster">
+                    <item jid="mercutio@montague.lit" name="New Name"><group>Other</group></item>
+                </query>
+            </iq>`);
+    }));
+
+    it("can be used to remove a contact",
+            mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
 
+        const { api } = _converse;
+        await mock.waitForRoster(_converse, 'current', 1);
+        api.trigger('rosterContactsFetched');
+
+        const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+        await mock.openChatBoxFor(_converse, contact_jid);
+        await u.waitUntil(() => _converse.chatboxes.length > 1);
+
+        const view = _converse.chatboxviews.get(contact_jid);
+        let show_modal_button = view.querySelector('.show-msg-author-modal');
+        show_modal_button.click();
+        let modal = api.modal.get('converse-user-details-modal');
+        await u.waitUntil(() => u.isVisible(modal));
+        modal.querySelector('#edit-tab').click();
         spyOn(view.model.contact, 'sendRosterRemoveStanza').and.callFake(callback => callback());
-        let remove_contact_button = modal.querySelector('button.remove-contact');
-        expect(u.isVisible(remove_contact_button)).toBeTruthy();
+        let remove_contact_button = await u.waitUntil(() => modal.querySelector('button.remove-contact'));
         remove_contact_button.click();
-        await u.waitUntil(() => modal.getAttribute('aria-hidden'), 1000);
-        await u.waitUntil(() => !u.isVisible(modal));
-        show_modal_button = view.querySelector('.show-user-details-modal');
+
+        modal = await u.waitUntil(() => document.querySelector('converse-confirm-modal'));
+        modal.querySelector('.btn-primary').click();
+        await u.waitUntil(() => modal.getAttribute('aria-hidden'));
+
+        show_modal_button = view.querySelector('.show-msg-author-modal');
         show_modal_button.click();
         remove_contact_button = modal.querySelector('button.remove-contact');
         expect(remove_contact_button === null).toBeTruthy();
@@ -42,15 +87,19 @@ describe("The User Details Modal", function () {
         const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
         await mock.openChatBoxFor(_converse, contact_jid)
         const view = _converse.chatboxviews.get(contact_jid);
-        let show_modal_button = view.querySelector('.show-user-details-modal');
+        let show_modal_button = view.querySelector('.show-msg-author-modal');
         show_modal_button.click();
         let modal = _converse.api.modal.get('converse-user-details-modal');
         await u.waitUntil(() => u.isVisible(modal), 2000);
+        modal.querySelector('#edit-tab').click();
         spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
 
-        spyOn(view.model.contact, 'sendRosterRemoveStanza').and.callFake((callback, errback) => errback());
-        let remove_contact_button = modal.querySelector('button.remove-contact');
+        spyOn(view.model.contact, 'sendRosterRemoveStanza').and.callFake(() => {
+            throw new Error('foo')
+        });
+        let remove_contact_button = await u.waitUntil(() => modal.querySelector('button.remove-contact'));
         expect(u.isVisible(remove_contact_button)).toBeTruthy();
+
         remove_contact_button.click();
         await u.waitUntil(() => !u.isVisible(modal))
         await u.waitUntil(() => u.isVisible(document.querySelector('.alert-danger')), 2000);
@@ -58,14 +107,15 @@ describe("The User Details Modal", function () {
         const header = document.querySelector('.alert-danger .modal-title');
         expect(header.textContent).toBe("Error");
         expect(u.ancestor(header, '.modal-content').querySelector('.modal-body p').textContent.trim())
-            .toBe("Sorry, there was an error while trying to remove Mercutio as a contact.");
-        document.querySelector('.alert-danger  button.close').click();
-        show_modal_button = view.querySelector('.show-user-details-modal');
+            .toBe("Sorry, an error occurred while trying to remove Mercutio as a contact");
+        document.querySelector('.alert-danger .btn-close').click();
+
+        show_modal_button = view.querySelector('.show-msg-author-modal');
         show_modal_button.click();
         modal = _converse.api.modal.get('converse-user-details-modal');
         await u.waitUntil(() => u.isVisible(modal), 2000)
 
-        show_modal_button = view.querySelector('.show-user-details-modal');
+        show_modal_button = view.querySelector('.show-msg-author-modal');
         show_modal_button.click();
         await u.waitUntil(() => u.isVisible(modal), 2000)
 

+ 1 - 1
src/shared/modals/user-details.js

@@ -78,7 +78,7 @@ export default class UserDetailsModal extends BaseModal {
         const data = new FormData(form);
         const name = /** @type {string} */ (data.get("name") || "").trim();
         const groups = /** @type {string} */(data.get('groups'))?.split(',').map((g) => g.trim()) || [];
-        this.model.contact.save({
+        this.model.contact.update({
             nickname: name,
             groups,
         });

+ 1 - 1
src/types/shared/modals/templates/user-details.d.ts

@@ -1,5 +1,5 @@
 /**
  * @param {import('../user-details').default} el
  */
-export function tplUserDetailsModal(el: import("../user-details").default): import("lit").TemplateResult<1>;
+export function tplUserDetailsModal(el: import("../user-details").default): import("lit").TemplateResult<1> | "";
 //# sourceMappingURL=user-details.d.ts.map

+ 1 - 1
src/types/shared/modals/user-details.d.ts

@@ -5,7 +5,7 @@ export default class UserDetailsModal extends BaseModal {
      * @param {Map<string, any>} changed
      */
     shouldUpdate(changed: Map<string, any>): boolean;
-    renderModal(): import("lit").TemplateResult<1>;
+    renderModal(): import("lit").TemplateResult<1> | "";
     getModalTitle(): any;
     registerContactEventHandlers(): void;
     /**