diff --git a/.eslintrc.json b/.eslintrc.json index 9e8ffb0a1..00925aaab 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -24,7 +24,7 @@ "ignoreMethods": [ "assign", "every", "keys", "find", "endsWith", "startsWith", "filter", "reduce", "isArray", "create", "map", "replace", "some", "toLower", - "split", "trim", "forEach", "toUpperCase", "includes", "values" + "split", "trim", "forEach", "toUpperCase", "includes", "values", "padStart" ] }], "lodash/import-scope": "off", diff --git a/package-lock.json b/package-lock.json index 8fa258c60..650fdae35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2673,8 +2673,8 @@ "dev": true }, "backbone.overview": { - "version": "github:jcbrand/Backbone.Overview#b3e759127d859c90e8e21700a9a5714a3b828f0a", - "from": "github:jcbrand/Backbone.Overview#b3e759127d859c90e8e21700a9a5714a3b828f0a", + "version": "github:jcbrand/Backbone.Overview#d83d0fc0e40aaf3fb5b4db81576b0eba46d6739a", + "from": "github:jcbrand/Backbone.Overview#d83d0fc0e40aaf3fb5b4db81576b0eba46d6739a", "dev": true, "requires": { "lodash": "^4.17.11" diff --git a/package.json b/package.json index c2b56a605..835afc038 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "@fortawesome/fontawesome-free": "5.3.1", "babel-loader": "^8.0.4", "backbone.nativeview": "conversejs/Backbone.NativeView#5997c8197ca594e6b8469447f28310c78bd1d95e", - "backbone.overview": "jcbrand/Backbone.Overview#b3e759127d859c90e8e21700a9a5714a3b828f0a", + "backbone.overview": "jcbrand/Backbone.Overview#d83d0fc0e40aaf3fb5b4db81576b0eba46d6739a", "backbone.vdomview": "^1.0.1", "bootstrap": "^4.0.0", "bootstrap.native": "^2.0.26", diff --git a/spec/bookmarks.js b/spec/bookmarks.js index cbabf43a6..ee2c23407 100644 --- a/spec/bookmarks.js +++ b/spec/bookmarks.js @@ -661,12 +661,14 @@ { hide_open_bookmarks: true }, async function (done, _converse) { + test_utils.openControlBox(); const jid = 'room@conference.example.org'; await test_utils.waitUntilDiscoConfirmed( _converse, _converse.bare_jid, [{'category': 'pubsub', 'type': 'pep'}], ['http://jabber.org/protocol/pubsub#publish-options'] ); + await test_utils.waitUntil(() => _converse.bookmarks); // XXX Create bookmarks view here, otherwise we need to mock stanza // traffic for it to get created. _converse.bookmarksview = new _converse.BookmarksView( @@ -682,13 +684,16 @@ 'nick': ' Othello' }); expect(_converse.bookmarks.length).toBe(1); - const room_els = _converse.bookmarksview.el.querySelectorAll(".open-room"); + + const bmarks_view = _converse.bookmarksview; + await test_utils.waitUntil(() => bmarks_view.el.querySelectorAll(".open-room").length, 500); + const room_els = bmarks_view.el.querySelectorAll(".open-room"); expect(room_els.length).toBe(1); - // Check that it disappears once the room is opened const bookmark = _converse.bookmarksview.el.querySelector(".open-room"); bookmark.click(); await test_utils.waitUntil(() => _converse.chatboxviews.get(jid)); + expect(u.hasClass('hidden', _converse.bookmarksview.el.querySelector(".available-chatroom"))).toBeTruthy(); // Check that it reappears once the room is closed const view = _converse.chatboxviews.get(jid); diff --git a/spec/chatbox.js b/spec/chatbox.js index 600566c9c..5f87fad53 100644 --- a/spec/chatbox.js +++ b/spec/chatbox.js @@ -1467,7 +1467,7 @@ _converse.chatboxes.onMessage(msgFactory()); await test_utils.waitUntil(() => chatbox.messages.length > 1); expect(select_msgs_indicator().textContent).toBe('2'); - view.maximize(); + view.model.maximize(); expect(select_msgs_indicator()).toBeUndefined(); done(); })); diff --git a/spec/muc.js b/spec/muc.js index 4b46935e9..ec5f41324 100644 --- a/spec/muc.js +++ b/spec/muc.js @@ -1484,8 +1484,8 @@ var name; const view = _converse.chatboxviews.get('lounge@montague.lit'); const occupants = view.el.querySelector('.occupant-list'); - var presence, role, jid, model; - for (var i=0; i occupants.querySelectorAll('li').length > 2, 500); + expect(occupants.querySelectorAll('li').length).toBe(1+mock.chatroom_names.length); + + mock.chatroom_names.forEach(name => { + const model = view.model.occupants.findWhere({'nick': name}); + const index = view.model.occupants.indexOf(model); + expect(occupants.querySelectorAll('li .occupant-nick')[index].textContent.trim()).toBe(name); + }); + // Test users leaving the groupchat // https://xmpp.org/extensions/xep-0045.html#exit - for (i=mock.chatroom_names.length-1; i>-1; i--) { + for (let i=mock.chatroom_names.length-1; i>-1; i--) { name = mock.chatroom_names[i]; role = mock.chatroom_roles[name].role; // See example 21 https://xmpp.org/extensions/xep-0045.html#enter-pres @@ -1554,12 +1559,17 @@ }).up() .c('status').attrs({code:'110'}).nodeTree; _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect(occupants.querySelectorAll('li').length).toBe(2+i); - model = view.model.occupants.where({'nick': name})[0]; - const index = view.model.occupants.indexOf(model); - expect(occupants.querySelectorAll('li .occupant-nick')[index].textContent.trim()).toBe(mock.chatroom_names[i]); } + await test_utils.waitUntil(() => occupants.querySelectorAll('li').length > 1, 500); + expect(occupants.querySelectorAll('li').length).toBe(1+mock.chatroom_names.length); + + mock.chatroom_names.forEach(name => { + const model = view.model.occupants.findWhere({'nick': name}); + const index = view.model.occupants.indexOf(model); + expect(occupants.querySelectorAll('li .occupant-nick')[index].textContent.trim()).toBe(name); + }); + // Test users leaving the groupchat // https://xmpp.org/extensions/xep-0045.html#exit for (i=mock.chatroom_names.length-1; i>-1; i--) { @@ -1607,6 +1617,7 @@ _converse.connection._dataRecv(test_utils.createRequest(presence)); const view = _converse.chatboxviews.get('lounge@montague.lit'); + await test_utils.waitUntil(() => view.el.querySelectorAll('li .occupant-nick').length, 500); const occupants = view.el.querySelector('.occupant-list').querySelectorAll('li .occupant-nick'); expect(occupants.length).toBe(2); expect(occupants[0].textContent.trim()).toBe("<img src="x" onerror="alert(123)"/>"); @@ -1622,7 +1633,8 @@ const view = _converse.chatboxviews.get('lounge@montague.lit'); let contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - let occupants = view.el.querySelector('.occupant-list').querySelectorAll('li'); + await test_utils.waitUntil(() => view.el.querySelectorAll('.occupant-list li').length, 500); + let occupants = view.el.querySelectorAll('.occupant-list li'); expect(occupants.length).toBe(1); expect(occupants[0].querySelector('.occupant-nick').textContent.trim()).toBe("romeo"); expect(occupants[0].querySelectorAll('.badge').length).toBe(2); @@ -1641,6 +1653,7 @@ .c('status').attrs({code:'110'}).nodeTree; _converse.connection._dataRecv(test_utils.createRequest(presence)); + await test_utils.waitUntil(() => view.el.querySelectorAll('.occupant-list li').length > 1, 500); occupants = view.el.querySelectorAll('.occupant-list li'); expect(occupants.length).toBe(2); expect(occupants[0].querySelector('.occupant-nick').textContent.trim()).toBe("moderatorman"); @@ -1665,6 +1678,7 @@ .c('status').attrs({code:'110'}).nodeTree; _converse.connection._dataRecv(test_utils.createRequest(presence)); + await test_utils.waitUntil(() => view.el.querySelectorAll('.occupant-list li').length > 2, 500); occupants = view.el.querySelector('.occupant-list').querySelectorAll('li'); expect(occupants.length).toBe(3); expect(occupants[2].querySelector('.occupant-nick').textContent.trim()).toBe("visitorwoman"); @@ -2209,6 +2223,7 @@ expect(view.model.get('connection_status')).toBe(converse.ROOMSTATUS.ENTERED); const chat_content = view.el.querySelector('.chat-content'); + await test_utils.waitUntil(() => view.el.querySelectorAll('li .occupant-nick').length, 500); let occupants = view.el.querySelector('.occupant-list'); expect(occupants.childNodes.length).toBe(1); expect(occupants.firstElementChild.querySelector('.occupant-nick').textContent.trim()).toBe("oldnick"); @@ -2840,7 +2855,6 @@ async function (done, _converse) { let iq_stanza; - await test_utils.openAndEnterChatRoom(_converse, 'lounge', 'muc.montague.lit', 'romeo'); const view = _converse.chatboxviews.get('lounge@muc.montague.lit'); /* We don't show join/leave messages for existing occupants. We @@ -2973,7 +2987,7 @@ "id": iq_stanza.getAttribute("id") }).c("query", {"xmlns": "http://jabber.org/protocol/muc#admin"}) _converse.connection._dataRecv(test_utils.createRequest(result)); - + await test_utils.waitUntil(() => view.el.querySelectorAll('.occupant').length, 500); await test_utils.waitUntil(() => view.el.querySelectorAll('.badge').length > 1); expect(view.model.occupants.length).toBe(2); expect(view.el.querySelectorAll('.occupant').length).toBe(2); diff --git a/spec/profiling.js b/spec/profiling.js index b01ed3d76..646cb9403 100644 --- a/spec/profiling.js +++ b/spec/profiling.js @@ -3,9 +3,40 @@ } (this, function (jasmine, mock, test_utils) { var _ = converse.env._; var $iq = converse.env.$iq; + var $pres = converse.env.$pres; var u = converse.env.utils; describe("Profiling", function() { + + it("shows users currently present in the groupchat", + mock.initConverse( + null, ['rosterGroupsFetched'], {'muc_show_join_leave': false}, + async function (done, _converse) { + + test_utils.openControlBox(); + await test_utils.openAndEnterChatRoom(_converse, 'lounge', 'montague.lit', 'romeo'); + const view = _converse.chatboxviews.get('lounge@montague.lit'), + occupants = view.el.querySelector('.occupant-list'); + _.rangeRight(3000, 0).forEach(i => { + const name = `User ${i.toString().padStart(5, '0')}`; + const presence = $pres({ + 'to': 'romeo@montague.lit/orchard', + 'from': 'lounge@montague.lit/'+name + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) + .c('item').attrs({ + affiliation: 'none', + jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit', + }); + _converse.connection._dataRecv(test_utils.createRequest(presence)); + + // expect(occupants.querySelectorAll('li').length).toBe(1+i); + // const model = view.model.occupants.where({'nick': name})[0]; + // const index = view.model.occupants.indexOf(model); + // expect(occupants.querySelectorAll('li .occupant-nick')[index].textContent.trim()).toBe(name); + }); + done(); + })); + xit("adds hundreds of contacts to the roster", mock.initConverse( null, ['rosterGroupsFetched'], {}, @@ -54,7 +85,7 @@ mock.initConverse( null, ['rosterGroupsFetched'], {}, function (done, _converse) { - + // _converse.show_only_online_users = true; _converse.roster_groups = true; test_utils.openControlBox(); diff --git a/spec/roomslist.js b/spec/roomslist.js index 9e67c01d7..88fd9d471 100644 --- a/spec/roomslist.js +++ b/spec/roomslist.js @@ -20,10 +20,15 @@ await test_utils.openChatRoom(_converse, 'room', 'conference.shakespeare.lit', 'JC'); expect(_.isUndefined(_converse.rooms_list_view)).toBeFalsy(); - let room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room"); + + const lview = _converse.rooms_list_view + await test_utils.waitUntil(() => lview.el.querySelectorAll(".open-room").length); + let room_els = lview.el.querySelectorAll(".open-room"); expect(room_els.length).toBe(1); expect(room_els[0].innerText).toBe('room@conference.shakespeare.lit'); + await test_utils.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo'); + await test_utils.waitUntil(() => lview.el.querySelectorAll(".open-room").length > 1); room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room"); expect(room_els.length).toBe(2); @@ -119,19 +124,23 @@ allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic. }, async function (done, _converse) { - let room_els, item; + let item; await _converse.api.rooms.open('coven@chat.shakespeare.lit', {'nick': 'some1'}); - room_els = _converse.rooms_list_view.el.querySelectorAll(".available-chatroom"); + + const lview = _converse.rooms_list_view + await test_utils.waitUntil(() => lview.el.querySelectorAll(".open-room").length); + let room_els = lview.el.querySelectorAll(".available-chatroom"); expect(room_els.length).toBe(1); item = room_els[0]; expect(u.hasClass('open', item)).toBe(true); expect(item.textContent.trim()).toBe('coven@chat.shakespeare.lit'); await _converse.api.rooms.open('balcony@chat.shakespeare.lit', {'nick': 'some1'}); - room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room"); + await test_utils.waitUntil(() => lview.el.querySelectorAll(".open-room").length > 1); + room_els = lview.el.querySelectorAll(".open-room"); expect(room_els.length).toBe(2); - room_els = _converse.rooms_list_view.el.querySelectorAll(".available-chatroom.open"); + room_els = lview.el.querySelectorAll(".available-chatroom.open"); expect(room_els.length).toBe(1); item = room_els[0]; expect(item.textContent.trim()).toBe('balcony@chat.shakespeare.lit'); @@ -258,9 +267,11 @@ expect(_converse.chatboxes.length).toBe(1); await test_utils.openChatRoom(_converse, 'lounge', 'conference.shakespeare.lit', 'JC'); expect(_converse.chatboxes.length).toBe(2); - var room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room"); + const lview = _converse.rooms_list_view + await test_utils.waitUntil(() => lview.el.querySelectorAll(".open-room").length); + let room_els = lview.el.querySelectorAll(".open-room"); expect(room_els.length).toBe(1); - var close_el = _converse.rooms_list_view.el.querySelector(".close-room"); + const close_el = _converse.rooms_list_view.el.querySelector(".close-room"); close_el.click(); expect(window.confirm).toHaveBeenCalledWith( 'Are you sure you want to leave the groupchat lounge@conference.shakespeare.lit?'); diff --git a/src/converse-rosterview.js b/src/converse-rosterview.js index 68a6d32ea..2e135a0b8 100644 --- a/src/converse-rosterview.js +++ b/src/converse-rosterview.js @@ -540,6 +540,7 @@ converse.plugins.add('converse-rosterview', { "click a.group-toggle": "toggle" }, + sortImmediatelyOnAdd: true, ItemView: _converse.RosterContactView, listItems: 'model.contacts', listSelector: '.roster-group-contacts', @@ -558,7 +559,7 @@ converse.plugins.add('converse-rosterview', { // assigned to their various groups. _converse.rosterview.on( 'rosterContactsFetchedAndProcessed', - this.sortAndPositionAllItems.bind(this) + () => this.sortAndPositionAllItems() ); }, @@ -597,7 +598,7 @@ converse.plugins.add('converse-rosterview', { */ let shown = 0; const all_contact_views = this.getAll(); - _.each(this.model.contacts.models, (contact) => { + this.model.contacts.forEach(contact => { const contact_view = this.get(contact.get('id')); if (_.includes(contacts, contact)) { u.hideElement(contact_view.el); @@ -730,6 +731,7 @@ converse.plugins.add('converse-rosterview', { listSelector: '.roster-contacts', sortEvent: null, // Groups are immutable, so they don't get re-sorted subviewIndex: 'name', + sortImmediatelyOnAdd: true, events: { 'click a.controlbox-heading__btn.add-contact': 'showAddContactModal', @@ -758,7 +760,7 @@ converse.plugins.add('converse-rosterview', { _converse.api.listen.on('rosterGroupsFetched', this.sortAndPositionAllItems.bind(this)); _converse.api.listen.on('rosterContactsFetched', () => { - _converse.roster.each((contact) => this.addRosterContact(contact, {'silent': true})); + _converse.roster.each(contact => this.addRosterContact(contact, {'silent': true})); this.update(); this.updateFilter(); this.trigger('rosterContactsFetchedAndProcessed');