diff --git a/public/js/event-views.js b/public/js/event-views.js index 26276c10..42ed508e 100644 --- a/public/js/event-views.js +++ b/public/js/event-views.js @@ -50,6 +50,13 @@ views.SessionView = Backbone.Marionette.ItemView.extend({ // re-render to show them. this.listenTo(this.model, 'change:connectedParticipants', this.render, this); this.listenTo(this.model, 'change:joiningParticipants', this.render, this); + // Maintain a list of slots and user preferences for them, so that we + // can render people in consistent-ish places in the list. + // The idea is that each user gets a "slotPreference", which is either + // the last slot they were rendered in. If their preferred slot is occupied, + // they get the next unused slot, and their "preference" is updated. + this.userSlotPreference = {}; + this.userSlots = {}; }, onRender: function() { @@ -78,9 +85,29 @@ views.SessionView = Backbone.Marionette.ItemView.extend({ // the hangout-users div, and populate it with users. this.$el.addClass("hangout-connected"); + // // Build the list of user views. + // + var fragment = document.createDocumentFragment(); - var drawUser = function (udata, joining) { + + // clear out slots for users that are no longer connected, and + // construct an array of any slots that are available. + var connectedAndJoining = _.pluck(this.model.get("connectedParticipants"), "id") + .concat(_.pluck(this.model.get("joiningParticipants"), "id")); + var available = []; + for (var i = 0; i < this.model.MAX_ATTENDEES; i++) { + if (this.userSlots[i]) { + if (_.contains(connectedAndJoining, this.userSlots[i].id)) { + continue; + } + delete this.userSlots[i]; + } + available.push(i); + } + + var drawUser = _.bind(function (udata, joining) { + // Get the user view. var userView; if (udata.id in userViewCache) { userView = userViewCache[udata.id]; @@ -97,18 +124,41 @@ views.SessionView = Backbone.Marionette.ItemView.extend({ if (joining) { el.className += " joining"; } - fragment.appendChild(el); - } - // Add connected users + + // Determine where it goes. + var slot = this.userSlots[this.userSlotPreference[udata.id]]; + if (slot && slot.id === udata.id) { + slot.el = el; + } else { + var pos = null; + var pref = this.userSlotPreference[udata.id]; + if (pref && _.contains(available, pref)) { + pos = pref; + available = _.without(available, pref); + } else if (available.length > 0) { + pos = available.shift(); + } + if (pos !== null) { + this.userSlotPreference[udata.id] = pos; + slot = {id: udata.id, el: el} + this.userSlots[pos] = slot; + } + } + }, this); + + // build slots for connected users _.each(this.model.get("connectedParticipants"), function(udata) { drawUser(udata); }); - // Add joining users + // ... and joining users _.each(this.model.get("joiningParticipants"), function(udata) { drawUser(udata, true); }); var emptyli; - for(var i = 0; i < this.model.MAX_ATTENDEES - numAttendees; i++) { - //this.ui.hangoutUsers.append($("
")); - emptyli = document.createElement("li"); - emptyli.className = "empty"; - fragment.appendChild(emptyli); + for (var i = 0; i < this.model.MAX_ATTENDEES; i++) { + if (this.userSlots[i]) { + fragment.appendChild(this.userSlots[i].el); + } else { + emptyli = document.createElement("li"); + emptyli.className = "empty"; + fragment.appendChild(emptyli); + } } // Now add the fragment to the layout and display it @@ -443,8 +493,6 @@ views.UserColumnLayout = Backbone.Marionette.Layout.extend({ }); // The actual core UserListView that manages displaying each individual user. -// This logic is quite similar to the SessionListView, which also deals with -// pagination in a flexible-height space. views.UserListView = Backbone.Marionette.CompositeView.extend({ template: '#user-list-template', itemView: views.UserView, @@ -464,15 +512,6 @@ views.UserListView = Backbone.Marionette.CompositeView.extend({ // are removed, but totalUnfilteredRecords does. Could // be a bug. - // Other side note: be aware that there is some magic in - // marionette around adding to collections. It apparently - // tries to just auto-add the new record to the - // itemViewContainer. This is a little weird when - // combined with the pagination system, which doesn't - // necessarily show all incoming models. Just something - // to keep an eye on. More info here: - // https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.compositeview.md#model-and-collection-rendering - this.$el.find(".header .contents").text(this.collection.length); }, this); }, diff --git a/test/test.admin-users.selenium.js b/test/test.admin-users.selenium.js index 458656ea..51378806 100644 --- a/test/test.admin-users.selenium.js +++ b/test/test.admin-users.selenium.js @@ -99,8 +99,9 @@ describe("ADMIN USERS SELENIUM", function() { // Wait for modal to fade in... browser.waitForSelector(".modal-body select"); browser.byCss(".modal-body select").sendKeys(event.get("title")); - browser.byLinkText("Add").click().then(function() { - expect(user.isAdminOf(event)).to.be(true); + browser.byLinkText("Add").click(); + browser.wait(function() { + return user.isAdminOf(event) === true; }); // Ensure the new admin can access the admin page. diff --git a/test/test.session-joining.selenium.js b/test/test.session-joining.selenium.js index 5ab07086..2850359c 100644 --- a/test/test.session-joining.selenium.js +++ b/test/test.session-joining.selenium.js @@ -430,4 +430,72 @@ describe("SESSION JOINING PARTICIPANT LISTS", function() { done(); }); }); + + it("Preserves session list position", function(done) { + this.timeout(80000); + // We'll use 3 sockets --- s1, s2, s3 in the session. + var s1, s2, s3; + var session = event.get("sessions").at(0); + var u1 = common.server.db.users.findWhere({"sock-key": "regular1"}); + var u2 = common.server.db.users.findWhere({"sock-key": "regular2"}); + var u3 = common.server.db.users.findWhere({"sock-key": "admin1"}); + var observer = common.server.db.users.findWhere({"sock-key": "admin2"}); + browser.get("http://localhost:7777/"); + browser.mockAuthenticate(observer.get("sock-key")); + browser.get("http://localhost:7777" + event.getEventUrl()); + browser.waitForScript("$"); + browser.then(function() { + return common.authedSock( + u1.getSockKey(), session.getRoomId() + ).then(function(sock) { s1 = sock; }); + }); + browser.then(function() { + return common.authedSock( + u2.getSockKey(), session.getRoomId() + ).then(function(sock) { s2 = sock; }); + }); + browser.then(function() { + return common.authedSock( + u3.getSockKey(), session.getRoomId() + ).then(function(sock) { s3 = sock; }); + }); + function checkSessionUsers(list) { + var selector ="[data-session-id='" + session.id + "'] ul.hangout-users li"; + return browser.wait(function() { + return browser.byCsss(selector).then(function(els) { + return Promise.all(_.map(els, function(el, i) { + return new Promise(function(resolve, reject) { + el.getAttribute("class").then(function(cls) { + if (list[i] === null && cls === "empty") { + return resolve(); + } else if (cls === "user focus") { + return resolve(); + } else { + return reject("Unexpected class " + cls); + } + }).then(null, function(err) { + return reject("Selenium error"); + }); + }); + })).catch(function() { + return false; + }); + }); + }); + }; + checkSessionUsers([u1, u2, u3, null, null, null, null, null, null, null]); + browser.then(function() { return s2.promiseClose(); }); + checkSessionUsers([u1, null, u3, null, null, null, null, null, null, null]); + browser.then(function() { return s1.promiseClose(); }); + checkSessionUsers([null, null, u3, null, null, null, null, null, null, null]); + browser.then(function() { + return common.authedSock( + u2.getSockKey(), session.getRoomId() + ).then(function(sock) { s2 = sock }); + }); + checkSessionUsers([null, u2, u3, null, null, null, null, null, null, null]); + browser.then(function() { return s3.promiseClose(); }) + browser.then(function() { return s2.promiseClose(); }); + browser.then(function() { done(); }); + }); });