From 824e9d2c7629cfc8f160b25b76f55f29d1a39bc4 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Wed, 6 Mar 2013 15:28:35 -0700 Subject: [PATCH] added PaginatedCollectionView test plan: 1. see the specs, there is no implementation yet Change-Id: I277300a507581cba4fe24682cad2d1ebbdf4147f Reviewed-on: https://gerrit.instructure.com/18361 Tested-by: Jenkins Reviewed-by: Joe Tanner QA-Review: Ryan Florence --- .../collections/PaginatedCollection.coffee | 2 +- .../views/PaginatedCollectionView.coffee | 141 ++++++++++++++++++ app/views/jst/paginatedCollection.handlebars | 10 ++ .../PaginatedCollectionSpec.coffee | 29 +--- spec/coffeescripts/helpers/getFakePage.coffee | 29 ++++ .../views/PaginatedCollectionViewSpec.coffee | 98 ++++++++++++ 6 files changed, 281 insertions(+), 28 deletions(-) create mode 100644 app/coffeescripts/views/PaginatedCollectionView.coffee create mode 100644 app/views/jst/paginatedCollection.handlebars create mode 100644 spec/coffeescripts/helpers/getFakePage.coffee create mode 100644 spec/coffeescripts/views/PaginatedCollectionViewSpec.coffee diff --git a/app/coffeescripts/collections/PaginatedCollection.coffee b/app/coffeescripts/collections/PaginatedCollection.coffee index 60c3de741d2..b8c20a8f1fa 100644 --- a/app/coffeescripts/collections/PaginatedCollection.coffee +++ b/app/coffeescripts/collections/PaginatedCollection.coffee @@ -24,7 +24,7 @@ define [ capitalize = (string = '') -> string.charAt(0).toUpperCase() + string.substring(1).toLowerCase() - class @PaginatedCollection extends Backbone.Collection + class PaginatedCollection extends Backbone.Collection # Matches the name of each link: "next," "prev," "first," or "last." nameRegex: /rel="([a-z]+)/ diff --git a/app/coffeescripts/views/PaginatedCollectionView.coffee b/app/coffeescripts/views/PaginatedCollectionView.coffee new file mode 100644 index 00000000000..c6a6fd7e90b --- /dev/null +++ b/app/coffeescripts/views/PaginatedCollectionView.coffee @@ -0,0 +1,141 @@ +define [ + 'jquery' + 'underscore' + 'compiled/views/CollectionView' + 'jst/paginatedCollection' +], ($, _, CollectionView, template) -> + + ## + # General purpose lazy-load view. It must have a PaginatedCollection. + # + # TODO: We should replace all PaginatedView instances with this + # + # example: + # + # new PaginatedCollectionView + # collection: somePaginatedCollection + # itemView: SomeItemView + + class PaginatedCollectionView extends CollectionView + + defaults: + + ## + # Distance to begin fetching the next page + + buffer: 500 + + ## + # Container with observed scroll position, can be a jQuery element, raw + # dom node, or selector + + scrollContainer: window + + ## + # Adds a loading indicator element + + els: _.extend({}, CollectionView::els, + '.loadingIndicator': '$loadingIndicator' + ) + + @optionProperty 'scrollContainer' + + template: template + + ## + # Initializes the view + + initialize: -> + super + @initScrollContainer() + @attachScroll() + + ## + # Extends parent to detach scroll container event + # + # @api private + + attachCollection: -> + super + @collection.on 'fetched:last', @detachScroll + @collection.on 'beforeFetch', @showLoadingIndicator + @collection.on 'fetch', @hideLoadingIndicator + + ## + # Sets instance properties regarding the scrollContainer + # + # @api private + + initScrollContainer: -> + @scrollContainer = $ @scrollContainer + @heightContainer = if @scrollContainer[0] is window + # window has no scrollHeight + document.body + else + @scrollContainer[0] + + ## + # Attaches scroll event to scrollContainer + # + # @api private + + attachScroll: -> + event = "scroll.pagination:#{@cid}, resize.pagination:#{@cid}" + @scrollContainer.on event, @checkScroll + + ## + # Removes the scoll event from scrollContainer + # + # @api private + + detachScroll: => + @scrollContainer.off ".pagination:#{@cid}" + + ## + # Determines if we need to fetch the collection's next page + # + # @api public + + checkScroll: => + return if @fetchingPage + distanceToBottom = @heightContainer.scrollHeight - + @scrollContainer.scrollTop() - + @scrollContainer.height() + if distanceToBottom < @options.buffer + @collection.fetch page: 'next' + + ## + # Remove scroll event if view is removed + # + # @api public + + remove: -> + @detachScroll() + super + + + ## + # Hides the loading indicator after render + # + # @api private + + afterRender: -> + super + @hideLoadingIndicator() + + ## + # Hides the loading indicator + # + # @api private + + hideLoadingIndicator: => + @$loadingIndicator.hide() + + ## + # Shows the loading indicator + # + # @api private + + showLoadingIndicator: => + @$loadingIndicator.show() + diff --git a/app/views/jst/paginatedCollection.handlebars b/app/views/jst/paginatedCollection.handlebars new file mode 100644 index 00000000000..f80b84e027b --- /dev/null +++ b/app/views/jst/paginatedCollection.handlebars @@ -0,0 +1,10 @@ +{{#if collection.length}} +
    +{{else}} +

    + {{#t "no_items"}}No items.{{/t}} +

    +{{/if}} + +
    + diff --git a/spec/coffeescripts/collections/PaginatedCollectionSpec.coffee b/spec/coffeescripts/collections/PaginatedCollectionSpec.coffee index a3b38560341..274434b54a0 100644 --- a/spec/coffeescripts/collections/PaginatedCollectionSpec.coffee +++ b/spec/coffeescripts/collections/PaginatedCollectionSpec.coffee @@ -1,33 +1,8 @@ define [ 'Backbone' 'compiled/collections/PaginatedCollection' -], (Backbone, PaginatedCollection) -> - - # helper to get a fake page from the "server", gives you some fake model data - # and the Link header, don't send it a page greater than 10 or less than 1 - getFakePage = (thisPage = 1) -> - url = (page) -> "/api/v1/context/2/resource?page=#{page}&per_page=2" - lastID = thisPage * 2 - urls = - first: url 1 - last: url 10 - links = [] - if thisPage < 10 - urls.next = url thisPage + 1 - links.push '<' + urls.next + '>; rel="next"' - if thisPage > 1 - urls.prev = url thisPage - 1 - links.push '<' + urls.prev + '>; rel="prev"' - links.push '<' + urls.first + '>; rel="first"' - links.push '<' + urls.last + '>; rel="last"' - - urls: urls - header: links.join ',' - data: [ - id: lastID - 1, foo: 'bar', baz: 'qux' - , - id: lastID, foo: 'bar', baz: 'qux' - ], + 'helpers/getFakePage' +], (Backbone, PaginatedCollection, getFakePage) -> module 'PaginatedCollection', setup: -> diff --git a/spec/coffeescripts/helpers/getFakePage.coffee b/spec/coffeescripts/helpers/getFakePage.coffee new file mode 100644 index 00000000000..a6d411d4bfc --- /dev/null +++ b/spec/coffeescripts/helpers/getFakePage.coffee @@ -0,0 +1,29 @@ +define -> + + # helper to get a fake page from the "server", gives you some fake model data + # and the Link header, don't send it a page greater than 10 or less than 1 + getFakePage = (thisPage = 1) -> + url = (page) -> "/api/v1/context/2/resource?page=#{page}&per_page=2" + lastID = thisPage * 2 + urls = + first: url 1 + last: url 10 + links = [] + if thisPage < 10 + urls.next = url thisPage + 1 + links.push '<' + urls.next + '>; rel="next"' + if thisPage > 1 + urls.prev = url thisPage - 1 + links.push '<' + urls.prev + '>; rel="prev"' + links.push '<' + urls.first + '>; rel="first"' + links.push '<' + urls.last + '>; rel="last"' + + urls: urls + header: links.join ',' + data: [ + id: lastID - 1, foo: 'bar', baz: 'qux' + , + id: lastID, foo: 'bar', baz: 'qux' + ] + + diff --git a/spec/coffeescripts/views/PaginatedCollectionViewSpec.coffee b/spec/coffeescripts/views/PaginatedCollectionViewSpec.coffee new file mode 100644 index 00000000000..bb191bd9590 --- /dev/null +++ b/spec/coffeescripts/views/PaginatedCollectionViewSpec.coffee @@ -0,0 +1,98 @@ +define [ + 'jquery' + 'compiled/collections/PaginatedCollection' + 'compiled/views/PaginatedCollectionView' + 'helpers/getFakePage' +], ($, PaginatedCollection, PaginatedCollectionView, fakePage) -> + + server = null + collection = null + view = null + fixtures = $ '#fixtures' + + createServer = -> + server = sinon.fakeServer.create() + server.sendPage = (page, url) -> + @respond 'GET', url, [200, { + 'Content-Type': 'application/json' + 'Link': page.header + }, JSON.stringify page.data] + + class ItemView extends Backbone.View + tagName: 'li' + template: ({id}) -> id + initialize: -> + # make some scrolly happen + @$el.css 'height', 100 + + class TestCollection extends PaginatedCollection + url: '/test' + + module 'PaginatedCollectionView', + setup: -> + fixtures.css height: 100, overflow: 'auto' + createServer() + collection = new TestCollection + view = new PaginatedCollectionView + collection: collection + itemView: ItemView + scrollContainer: fixtures + view.$el.appendTo fixtures + view.render() + + teardown: -> + server.restore() + fixtures.attr 'style', '' + view.remove() + + assertItemRendered = (id) -> + $match = view.$list.children().filter (i, el) -> el.innerHTML is id + ok $match.length, 'item found' + + scrollToBottom = -> + fixtures[0].scrollTop = fixtures[0].scrollHeight + ok fixtures[0].scrollTop > 0 + + test 'renders items', -> + collection.add id: 1 + assertItemRendered '1' + + test 'renders items on collection fetch and fetch next', -> + collection.fetch() + server.sendPage fakePage(), collection.url + assertItemRendered '1' + assertItemRendered '2' + collection.fetch page: 'next' + server.sendPage fakePage(2), collection.urls.next + assertItemRendered '3' + assertItemRendered '4' + + test 'fetches the next page on scroll', -> + collection.fetch() + server.sendPage fakePage(), collection.url + scrollToBottom() + # scroll event isn't firing in the test :( manually calling checkScroll + view.checkScroll() + ok collection.fetchingNextPage, 'collection is fetching' + server.sendPage fakePage(2), collection.urls.next + assertItemRendered '3' + assertItemRendered '4' + + test 'stops fetching pages after the last page', -> + # see later in the test why this exists + fakeEvent = "foo.pagination:#{view.cid}" + fixtures.on fakeEvent, -> + ok false, 'this should never run' + collection.fetch() + server.sendPage fakePage(), collection.url + for i in [2..10] + collection.fetch page: 'next' + server.sendPage fakePage(i), collection.urls.next + assertItemRendered '1' + assertItemRendered '20' + # this is ghetto, but data('events') is no longer around and I can't get + # the scroll events to trigger, but this works because the + # ".pagination:#{view.cid}" events are all wipe out on last fetch, so the + # assertion at the beginning of the test in the handler shouldn't fire + fixtures.trigger fakeEvent +