diff --git a/app/assets/javascripts/app/pages/contacts.js b/app/assets/javascripts/app/pages/contacts.js index 7c041026ce1ff5ffbbd67176bd391819905c4ae6..eca20fa330ae908d3b09eae6180a094f91043944 100644 --- a/app/assets/javascripts/app/pages/contacts.js +++ b/app/assets/javascripts/app/pages/contacts.js @@ -8,7 +8,6 @@ app.pages.Contacts = Backbone.View.extend({ "click #contacts_visibility_toggle" : "toggleContactVisibility", "click #chat_privilege_toggle" : "toggleChatPrivilege", "click #change_aspect_name" : "showAspectNameForm", - "keyup #contact_list_search" : "searchContactList", "click .conversation_button": "showMessageModal", "click #invitations-button": "showInvitationsModal" }, @@ -79,10 +78,6 @@ app.pages.Contacts = Backbone.View.extend({ $(".header > h3").show(); }, - searchContactList: function(e) { - this.stream.search($(e.target).val()); - }, - showMessageModal: function(){ app.helpers.showModal("#conversationModal"); }, diff --git a/app/assets/javascripts/app/router.js b/app/assets/javascripts/app/router.js index 6c27e8b5c634d31f82e4a10197fbcb148be323c7..5f8a3cc9640de5de36dde93b60848cc88171783d 100644 --- a/app/assets/javascripts/app/router.js +++ b/app/assets/javascripts/app/router.js @@ -81,13 +81,14 @@ app.Router = Backbone.Router.extend({ ).render(); }, - contacts: function() { + contacts: function(params) { app.aspect = new app.models.Aspect(gon.preloads.aspect); this._loadContacts(); var stream = new app.views.ContactStream({ collection: app.contacts, - el: $(".stream.contacts #contact_stream") + el: $(".stream.contacts #contact_stream"), + urlParams: params }); app.page = new app.pages.Contacts({stream: stream}); diff --git a/app/assets/javascripts/app/views/contact_stream_view.js b/app/assets/javascripts/app/views/contact_stream_view.js index ad93301bd809be1ff5c92f97dee0d5e0ebd022cf..cbc254c37605b0b255843fee8d7dac7e34e50977 100644 --- a/app/assets/javascripts/app/views/contact_stream_view.js +++ b/app/assets/javascripts/app/views/contact_stream_view.js @@ -1,77 +1,79 @@ // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later app.views.ContactStream = Backbone.View.extend({ - initialize: function() { - this.itemCount = 0; - this.perPage = 25; - this.query = ''; - this.resultList = this.collection.toArray(); + initialize: function(opts) { + this.page = 1; var throttledScroll = _.throttle(_.bind(this.infScroll, this), 200); $(window).scroll(throttledScroll); - this.on('renderContacts', this.renderContacts, this); + this.on("fetchContacts", this.fetchContacts, this); + this.urlParams = opts.urlParams; }, render: function() { - if( _.isEmpty(this.resultList) ) { - var content = document.createDocumentFragment(); - content = '<div id="no_contacts" class="well">' + - ' <h4>' + - Diaspora.I18n.t('contacts.search_no_results') + - ' </h4>' + - '</div>'; - this.$el.html(content); - } else { - this.$el.html(''); - this.renderContacts(); - } + this.fetchContacts(); }, - renderContacts: function() { + fetchContacts: function() { this.$el.addClass("loading"); - var content = document.createDocumentFragment(); - _.rest(_.first(this.resultList , this.itemCount + this.perPage), this.itemCount).forEach( function(item) { - var view = new app.views.Contact({model: item}); - content.appendChild(view.render().el); + $("#paginate .loader").removeClass("hidden"); + $.ajax(this._fetchUrl(), { + context: this + }).success(function(response) { + if (response.length === 0) { + this.onEmptyResponse(); + } else { + this.appendContactViews(response); + this.page++; + } }); + }, - var size = _.size(this.resultList); - if( this.itemCount + this.perPage >= size ){ - this.itemCount = size; - this.off('renderContacts'); - } else { - this.itemCount += this.perPage; + _fetchUrl: function() { + var url = Routes.contacts({format: "json", page: this.page}); + if (this.urlParams) { + url += "&" + this.urlParams; } - this.$el.append(content); - this.$el.removeClass("loading"); + return url; }, - search: function(query) { - query = query.trim(); - if( query || this.query ) { - this.off('renderContacts'); - this.on('renderContacts', this.renderContacts, this); - this.itemCount = 0; - if( query ) { - this.query = query; - var regex = new RegExp(query,'i'); - this.resultList = this.collection.filter(function(contact) { - return regex.test(contact.get('person').name) || - regex.test(contact.get('person').diaspora_id); - }); - } else { - this.resultList = this.collection.toArray(); - this.query = ''; - } - this.render(); + onEmptyResponse: function() { + if (this.collection.length === 0) { + var content = document.createDocumentFragment(); + content = "<div id='no_contacts' class='well'>" + + " <h4>" + + Diaspora.I18n.t("contacts.search_no_results") + + " </h4>" + + "</div>"; + this.$el.html(content); } + this.off("fetchContacts"); + this.$el.removeClass("loading"); + $("#paginate .loader").addClass("hidden"); + }, + + appendContactViews: function(contacts) { + var content = document.createDocumentFragment(); + contacts.forEach(function(contactData) { + var contact = new app.models.Contact(contactData); + this.collection.add(contact); + var view = new app.views.Contact({model: contact}); + content.appendChild(view.render().el); + }.bind(this)); + this.$el.append(content); + this.$el.removeClass("loading"); + $("#paginate .loader").addClass("hidden"); }, infScroll: function() { - if( this.$el.hasClass('loading') ) return; + if (this.$el.hasClass("loading")) { + return; + } var distanceTop = $(window).height() + $(window).scrollTop(), distanceBottom = $(document).height() - distanceTop; - if(distanceBottom < 300) this.trigger('renderContacts'); + if (distanceBottom < 300) { + this.trigger("fetchContacts"); + } } }); // @license-end diff --git a/app/assets/stylesheets/contacts.scss b/app/assets/stylesheets/contacts.scss index d5025275703e6ad5d02d77eade32356b07284d59..aaef928f7cac9ea9af6f07a555e5f13348b9344a 100644 --- a/app/assets/stylesheets/contacts.scss +++ b/app/assets/stylesheets/contacts.scss @@ -10,8 +10,13 @@ margin-bottom: 11px; margin-top: 11px; } + } - .aspect-controls { margin: 7px -10px 7px 0; } + .stream.contacts .aspect-controls { + margin-bottom: 7px; + margin-left: 30px; + margin-right: -10px; + margin-top: 7px; } } @@ -23,7 +28,7 @@ display: none; } #contact_list_search { - margin: 11px 30px 0 0; + margin: 11px 0 0; width: 150px; &:focus { width: 250px; } } diff --git a/app/controllers/contacts_controller.rb b/app/controllers/contacts_controller.rb index d3734da9b765ba0fddbd0c273b634791b03d824c..a0be1dcec673b2d8b4e8d3e5b20014d408261c9f 100644 --- a/app/controllers/contacts_controller.rb +++ b/app/controllers/contacts_controller.rb @@ -14,9 +14,13 @@ class ContactsController < ApplicationController # Used by the mobile site format.mobile { set_up_contacts_mobile } - # Used for mentions in the publisher + # Used for mentions in the publisher and pagination on the contacts page format.json { - @people = Person.search(params[:q], current_user, only_contacts: true).limit(15) + @people = if params[:q].present? + Person.search(params[:q], current_user, only_contacts: true).limit(15) + else + set_up_contacts_json + end render json: @people } end @@ -30,30 +34,54 @@ class ContactsController < ApplicationController private def set_up_contacts + if params[:a_id].present? + @aspect = current_user.aspects.find(params[:a_id]) + gon.preloads[:aspect] = AspectPresenter.new(@aspect).as_json + end + @contacts_size = current_user.contacts.size + end + + def set_up_contacts_json type = params[:set].presence - type ||= "by_aspect" if params[:a_id].present? + if params[:a_id].present? + type ||= "by_aspect" + @aspect = current_user.aspects.find(params[:a_id]) + end type ||= "receiving" - - @contacts = contacts_by_type(type) - @contacts_size = @contacts.length - gon.preloads[:contacts] = @contacts.map {|c| ContactPresenter.new(c, current_user).full_hash_with_person } + contacts_by_type(type).paginate(page: params[:page], per_page: 25) + .map {|c| ContactPresenter.new(c, current_user).full_hash_with_person } end def contacts_by_type(type) - case type + order = ["profiles.first_name ASC", "profiles.last_name ASC", "profiles.diaspora_handle ASC"] + contacts = case type when "all" + order.unshift "receiving DESC" current_user.contacts when "only_sharing" current_user.contacts.only_sharing when "receiving" current_user.contacts.receiving when "by_aspect" - @aspect = current_user.aspects.find(params[:a_id]) - gon.preloads[:aspect] = AspectPresenter.new(@aspect).as_json - current_user.contacts + order.unshift "contact_id IS NOT NULL DESC" + contacts_by_aspect(@aspect.id) else raise ArgumentError, "unknown type #{type}" end + contacts.includes(person: :profile) + .order(order) + end + + def contacts_by_aspect(aspect_id) + contacts = current_user.contacts.arel_table + aspect_memberships = AspectMembership.arel_table + current_user.contacts.joins( + contacts.outer_join(aspect_memberships).on( + aspect_memberships[:aspect_id].eq(aspect_id).and( + aspect_memberships[:contact_id].eq(contacts[:id]) + ) + ).join_sources + ) end def set_up_contacts_mobile diff --git a/app/views/contacts/_header.html.haml b/app/views/contacts/_header.html.haml index e6059e0051304bf74406a904292c24941063aa99..d8d323fb121907ac3cd381f57b233a80073519aa 100644 --- a/app/views/contacts/_header.html.haml +++ b/app/views/contacts/_header.html.haml @@ -20,7 +20,11 @@ = link_to @aspect, method: "delete", data: { confirm: t("aspects.edit.confirm_remove_aspect") }, class: "delete contacts_button", id: "delete_aspect" do %i.entypo-trash.contacts-header-icon{title: t("delete")} .pull-right.contact-list-search - = search_field_tag :contact_search, "", id: "contact_list_search", class: "search-query form-control", placeholder: t("contacts.index.user_search") + %form#contact-search-form{role: "search", method: "get", action: "/search"} + = search_field_tag :q, "", + id: "contact_list_search", + class: "search-query form-control", + placeholder: t("contacts.index.user_search") %h3 %span#aspect_name = @aspect.name @@ -32,6 +36,13 @@ = aspect.submit t('aspects.edit.update'), 'data-disable-with' => t('aspects.edit.updating'), class: "btn btn-default" - else + .pull-right.contact-list-search + %form#contact-search-form{role: "search", method: "get", action: "/search"} + = search_field_tag :q, "", + id: "contact_list_search", + class: "search-query form-control", + placeholder: t("contacts.index.user_search") + %h3 - case params["set"] - when "only_sharing" diff --git a/app/views/contacts/index.html.haml b/app/views/contacts/index.html.haml index ebc6d2e43b8d6d5ddc19ce52c0b8f94a80a59d5a..2a8de4f3265af6941543513f12cf5728e30c3730 100644 --- a/app/views/contacts/index.html.haml +++ b/app/views/contacts/index.html.haml @@ -29,6 +29,10 @@ .btn.btn-link{ 'data-toggle' => 'modal' } = t('invitations.new.invite_someone_to_join') + #paginate + %span.loader.hidden + .spinner + -if @aspect #new_conversation_pane = render 'shared/modal', diff --git a/spec/controllers/contacts_controller_spec.rb b/spec/controllers/contacts_controller_spec.rb index 27f9b9def179b0cf737e7506bdb4d89a22de1620..7a8ab7a13c80cc47602f9c4022383c8f6b5462dc 100644 --- a/spec/controllers/contacts_controller_spec.rb +++ b/spec/controllers/contacts_controller_spec.rb @@ -24,51 +24,97 @@ describe ContactsController, :type => :controller do expect(response).to be_success end - it "assigns contacts" do + it "doesn't assign contacts" do get :index contacts = assigns(:contacts) - expect(contacts.to_set).to eq(bob.contacts.to_set) - end - - it "shows only contacts a user is sharing with" do - contact = bob.contacts.first - contact.update_attributes(:sharing => false) - - get :index - contacts = assigns(:contacts) - expect(contacts.to_set).to eq(bob.contacts.receiving.to_set) - end - - it "shows all contacts (sharing and receiving)" do - contact = bob.contacts.first - contact.update_attributes(:sharing => false) - - get :index, :set => "all" - contacts = assigns(:contacts) - expect(contacts.to_set).to eq(bob.contacts.to_set) + expect(contacts).to be_nil end end context "format json" do - before do - @person1 = FactoryGirl.create(:person) - bob.share_with(@person1, bob.aspects.first) - @person2 = FactoryGirl.create(:person) - end - - it "succeeds" do - get :index, q: @person1.first_name, format: "json" - expect(response).to be_success - end - - it "responds with json" do - get :index, q: @person1.first_name, format: "json" - expect(response.body).to eq([@person1].to_json) + context "for the contacts search" do + before do + @person1 = FactoryGirl.create(:person) + bob.share_with(@person1, bob.aspects.first) + @person2 = FactoryGirl.create(:person) + end + + it "succeeds" do + get :index, q: @person1.first_name, format: "json" + expect(response).to be_success + end + + it "responds with json" do + get :index, q: @person1.first_name, format: "json" + expect(response.body).to eq([@person1].to_json) + end + + it "only returns contacts" do + get :index, q: @person2.first_name, format: "json" + expect(response.body).to eq([].to_json) + end end - it "only returns contacts" do - get :index, q: @person2.first_name, format: "json" - expect(response.body).to eq([].to_json) + context "for pagination on the contacts page" do + context "without parameters" do + it "returns contacts" do + get :index, format: "json", page: "1" + contact_ids = JSON.parse(response.body).map {|c| c["id"] } + expect(contact_ids.to_set).to eq(bob.contacts.map(&:id).to_set) + end + + it "returns only contacts which are receiving (the user is sharing with them)" do + contact = bob.contacts.first + contact.update_attributes(receiving: false) + + get :index, format: "json", page: "1" + contact_ids = JSON.parse(response.body).map {|c| c["id"] } + expect(contact_ids.to_set).to eq(bob.contacts.receiving.map(&:id).to_set) + expect(contact_ids).not_to include(contact.id) + end + end + + context "set: all" do + before do + contact = bob.contacts.first + contact.update_attributes(receiving: false) + end + + it "returns all contacts (sharing and receiving)" do + get :index, format: "json", page: "1", set: "all" + contact_ids = JSON.parse(response.body).map {|c| c["id"] } + expect(contact_ids.to_set).to eq(bob.contacts.map(&:id).to_set) + end + + it "sorts contacts by receiving status" do + get :index, format: "json", page: "1", set: "all" + contact_ids = JSON.parse(response.body).map {|c| c["id"] } + expect(contact_ids).to eq(bob.contacts.order("receiving DESC").map(&:id)) + expect(contact_ids.last).to eq(bob.contacts.first.id) + end + end + + context "with an aspect id" do + before do + @aspect = bob.aspects.create(name: "awesome contacts") + @person = FactoryGirl.create(:person) + bob.share_with(@person, @aspect) + end + + it "returns all contacts" do + get :index, format: "json", a_id: @aspect.id, page: "1" + contact_ids = JSON.parse(response.body).map {|c| c["id"] } + expect(contact_ids.to_set).to eq(bob.contacts.map(&:id).to_set) + end + + it "sorts contacts by aspect memberships" do + get :index, format: "json", a_id: @aspect.id, page: "1" + expect(JSON.parse(response.body).first["person"]["id"]).to eq(@person.id) + + get :index, format: "json", a_id: bob.aspects.first.id, page: "1" + expect(JSON.parse(response.body).first["person"]["id"]).not_to eq(@person.id) + end + end end end end diff --git a/spec/controllers/jasmine_fixtures/contacts_spec.rb b/spec/controllers/jasmine_fixtures/contacts_spec.rb index 261d3a88d69a16735d16c530ba59d82bc19af2b8..3e6cb3c85e1b3872fd802799c0b5708214a7ff26 100644 --- a/spec/controllers/jasmine_fixtures/contacts_spec.rb +++ b/spec/controllers/jasmine_fixtures/contacts_spec.rb @@ -17,7 +17,11 @@ describe ContactsController, :type => :controller do it "generates the aspects_manage fixture", :fixture => true do get :index, :a_id => @aspect.id save_fixture(html_for("body"), "aspects_manage") - save_fixture(controller.gon.preloads[:contacts].to_json, "aspects_manage_contacts_json") + end + + it "generates the aspects_manage_contacts_json fixture", fixture: true do + get :index, format: :json, a_id: @aspect.id, page: "1" + save_fixture(response.body, "aspects_manage_contacts_json") end it "generates the contacts_json fixture", :fixture => true do diff --git a/spec/javascripts/app/pages/contacts_spec.js b/spec/javascripts/app/pages/contacts_spec.js index 961378a5c80af0a25c9051f443d37b69964b6322..a40b899d22eaacf9cf407af1a448ad311a1a5d44 100644 --- a/spec/javascripts/app/pages/contacts_spec.js +++ b/spec/javascripts/app/pages/contacts_spec.js @@ -88,19 +88,6 @@ describe("app.pages.Contacts", function(){ }); }); - context('search contact list', function() { - beforeEach(function() { - this.searchinput = $('#contact_list_search'); - }); - - it('calls stream.search', function() { - this.view.stream.search = jasmine.createSpy(); - this.searchinput.val("Username"); - this.searchinput.trigger('keyup'); - expect(this.view.stream.search).toHaveBeenCalledWith("Username"); - }); - }); - describe("updateBadgeCount", function() { it("increases the badge count of an aspect", function() { var aspect = $("#aspect_nav .aspect").eq(0); diff --git a/spec/javascripts/app/views/contact_stream_view_spec.js b/spec/javascripts/app/views/contact_stream_view_spec.js index 955dd2e7b16f7feac26721b8fd83e65b22fdb800..7ae93bb5352e6252b6a2a589f9d913fd16057d1e 100644 --- a/spec/javascripts/app/views/contact_stream_view_spec.js +++ b/spec/javascripts/app/views/contact_stream_view_spec.js @@ -2,76 +2,199 @@ describe("app.views.ContactStream", function() { beforeEach(function() { loginAs({name: "alice", avatar : {small : "http://avatar.com/photo.jpg"}}); spec.loadFixture("aspects_manage"); - this.contacts = new app.collections.Contacts($.parseJSON(spec.readFixture("contacts_json"))); - app.aspect = new app.models.Aspect(this.contacts.first().get('aspect_memberships')[0].aspect); + this.contacts = new app.collections.Contacts(); + this.contactsData = $.parseJSON(spec.readFixture("contacts_json")); + app.aspect = new app.models.Aspect(this.contactsData[0].aspect_memberships[0].aspect); this.view = new app.views.ContactStream({ collection : this.contacts, - el: $('.stream.contacts #contact_stream') + el: $(".stream.contacts #contact_stream"), + urlParams: "set=all" }); - - this.view.perPage=1; - - //clean the page - this.view.$el.html(''); }); describe("initialize", function() { it("binds an infinite scroll listener", function() { spyOn($.fn, "scroll"); - new app.views.ContactStream({collection : this.contacts}); + new app.views.ContactStream({collection: this.contacts}); expect($.fn.scroll).toHaveBeenCalled(); }); + + it("binds 'fetchContacts'", function() { + spyOn(app.views.ContactStream.prototype, "fetchContacts"); + this.view = new app.views.ContactStream({collection: this.contacts}); + this.view.trigger("fetchContacts"); + expect(app.views.ContactStream.prototype.fetchContacts).toHaveBeenCalled(); + }); + + it("sets the current page for pagination to 1", function() { + expect(this.view.page).toBe(1); + }); + + it("sets urlParams to the given value", function() { + expect(this.view.urlParams).toBe("set=all"); + }); }); - describe("search", function() { - it("filters the contacts", function() { + describe("render", function() { + it("calls fetchContacts", function() { + spyOn(this.view, "fetchContacts"); this.view.render(); - expect(this.view.$el.html()).toContain("alice"); - this.view.search("eve"); - expect(this.view.$el.html()).not.toContain("alice"); - expect(this.view.$el.html()).toContain("eve"); + expect(this.view.fetchContacts).toHaveBeenCalled(); }); }); - describe("infScroll", function() { - beforeEach(function() { - this.view.off("renderContacts"); - this.fn = jasmine.createSpy(); - this.view.on("renderContacts", this.fn); - spyOn($.fn, "height").and.returnValue(0); - spyOn($.fn, "scrollTop").and.returnValue(100); + describe("fetchContacts", function() { + it("adds the loading class", function() { + expect(this.view.$el).not.toHaveClass("loading"); + this.view.fetchContacts(); + expect(this.view.$el).toHaveClass("loading"); }); - it("triggers renderContacts when the user is at the bottom of the page", function() { - this.view.infScroll(); - expect(this.fn).toHaveBeenCalled(); + it("displays the loading spinner", function() { + expect($("#paginate .loader")).toHaveClass("hidden"); + this.view.fetchContacts(); + expect($("#paginate .loader")).not.toHaveClass("hidden"); + }); + + it("calls $.ajax with the URL given by _fetchUrl", function() { + spyOn(this.view, "_fetchUrl").and.returnValue("/myAwesomeFetchUrl?foo=bar"); + this.view.fetchContacts(); + expect(jasmine.Ajax.requests.mostRecent().url).toBe("/myAwesomeFetchUrl?foo=bar"); + }); + + it("calls onEmptyResponse on an empty response", function() { + spyOn(this.view, "onEmptyResponse"); + this.view.fetchContacts(); + jasmine.Ajax.requests.mostRecent().respondWith({status: 200, responseText: JSON.stringify([])}); + expect(this.view.onEmptyResponse).toHaveBeenCalled(); + }); + + it("calls appendContactViews on a non-empty response", function() { + spyOn(this.view, "appendContactViews"); + this.view.fetchContacts(); + jasmine.Ajax.requests.mostRecent().respondWith({status: 200, responseText: JSON.stringify(this.contactsData)}); + expect(this.view.appendContactViews).toHaveBeenCalledWith(this.contactsData); + }); + + it("increases the current page on a non-empty response", function() { + this.view.page = 42; + this.view.fetchContacts(); + jasmine.Ajax.requests.mostRecent().respondWith({status: 200, responseText: JSON.stringify(this.contactsData)}); + expect(this.view.page).toBe(43); }); }); - describe("render", function() { - beforeEach(function() { - spyOn(this.view, "renderContacts"); + describe("_fetchUrl", function() { + it("returns the correct URL to fetch contacts", function() { + this.view.page = 15; + this.view.urlParams = undefined; + expect(this.view._fetchUrl()).toBe("/contacts.json?page=15"); }); - it("calls renderContacts", function() { - this.view.render(); - expect(this.view.renderContacts).toHaveBeenCalled(); + it("appends urlParams if those are set", function() { + this.view.page = 23; + expect(this.view._fetchUrl()).toBe("/contacts.json?page=23&set=all"); }); }); - describe("renderContacts", function() { - beforeEach(function() { - this.view.off("renderContacts"); - this.view.renderContacts(); + describe("onEmptyResponse", function() { + context("with an empty collection", function() { + it("adds a 'no contacts' div", function() { + this.view.onEmptyResponse(); + expect(this.view.$("#no_contacts").text().trim()).toBe(Diaspora.I18n.t("contacts.search_no_results")); + }); + + it("hides the loading spinner", function() { + this.view.$el.addClass("loading"); + $("#paginate .loader").removeClass("hidden"); + this.view.onEmptyResponse(); + expect(this.view.$el).not.toHaveClass("loading"); + expect($("#paginate .loader")).toHaveClass("hidden"); + }); + + it("unbinds 'fetchContacts'", function() { + spyOn(this.view, "off"); + this.view.onEmptyResponse(); + expect(this.view.off).toHaveBeenCalledWith("fetchContacts"); + }); + }); + + context("with a non-empty collection", function() { + beforeEach(function() { + this.view.collection.add(factory.contact()); + }); + + it("adds no 'no contacts' div", function() { + this.view.onEmptyResponse(); + expect(this.view.$("#no_contacts").length).toBe(0); + }); + + it("hides the loading spinner", function() { + this.view.$el.addClass("loading"); + $("#paginate .loader").removeClass("hidden"); + this.view.onEmptyResponse(); + expect(this.view.$el).not.toHaveClass("loading"); + expect($("#paginate .loader")).toHaveClass("hidden"); + }); + + it("unbinds 'fetchContacts'", function() { + spyOn(this.view, "off"); + this.view.onEmptyResponse(); + expect(this.view.off).toHaveBeenCalledWith("fetchContacts"); + }); + }); + }); + + describe("appendContactViews", function() { + it("hides the loading spinner", function() { + this.view.$el.addClass("loading"); + $("#paginate .loader").removeClass("hidden"); + this.view.appendContactViews(this.contactsData); + expect(this.view.$el).not.toHaveClass("loading"); + expect($("#paginate .loader")).toHaveClass("hidden"); }); - it("renders perPage contacts", function() { - expect(this.view.$el.find('.stream_element.contact').length).toBe(1); + it("adds all contacts to an empty collection", function() { + expect(this.view.collection.length).toBe(0); + this.view.appendContactViews(this.contactsData); + expect(this.view.collection.length).toBe(this.contactsData.length); + expect(this.view.collection.pluck("id")).toEqual(_.pluck(this.contactsData, "id")); }); - it("renders more contacts when called a second time", function() { - this.view.renderContacts(); - expect(this.view.$el.find('.stream_element.contact').length).toBe(2); + it("appends contacts to an existing collection", function() { + this.view.collection.add(this.contactsData[0]); + expect(this.view.collection.length).toBe(1); + this.view.appendContactViews(_.rest(this.contactsData)); + expect(this.view.collection.length).toBe(this.contactsData.length); + expect(this.view.collection.pluck("id")).toEqual(_.pluck(this.contactsData, "id")); + }); + + it("renders all added contacts", function() { + expect(this.view.$(".stream_element.contact").length).toBe(0); + this.view.appendContactViews(this.contactsData); + expect(this.view.$(".stream_element.contact").length).toBe(this.contactsData.length); + }); + + it("appends contacts to an existing contact list", function() { + this.view.appendContactViews([this.contactsData[0]]); + expect(this.view.$(".stream_element.contact").length).toBe(1); + this.view.appendContactViews(_.rest(this.contactsData)); + expect(this.view.$(".stream_element.contact").length).toBe(this.contactsData.length); + }); + }); + + describe("infScroll", function() { + beforeEach(function() { + this.view.off("fetchContacts"); + this.fn = jasmine.createSpy(); + this.view.on("fetchContacts", this.fn); + spyOn($.fn, "height").and.returnValue(0); + spyOn($.fn, "scrollTop").and.returnValue(100); + }); + + it("triggers fetchContacts when the user is at the bottom of the page", function() { + this.view.infScroll(); + expect(this.fn).toHaveBeenCalled(); }); }); });