From f71e78b69f890ac0f9c68fe308131034cb9034cc Mon Sep 17 00:00:00 2001 From: jsa Date: Tue, 22 Apr 2014 16:47:38 -0400 Subject: [PATCH] add acceptance tests for user profile pagination. JIRA: FOR-492 --- common/djangoapps/terrain/stubs/comments.py | 32 +++++- common/test/acceptance/fixtures/discussion.py | 16 +++ .../test/acceptance/pages/lms/discussion.py | 73 ++++++++++++ .../test/acceptance/tests/test_discussion.py | 108 +++++++++++++++++- 4 files changed, 225 insertions(+), 4 deletions(-) diff --git a/common/djangoapps/terrain/stubs/comments.py b/common/djangoapps/terrain/stubs/comments.py index eaca957935..80d209abf4 100644 --- a/common/djangoapps/terrain/stubs/comments.py +++ b/common/djangoapps/terrain/stubs/comments.py @@ -8,8 +8,14 @@ from .http import StubHttpRequestHandler, StubHttpService class StubCommentsServiceHandler(StubHttpRequestHandler): + + @property + def _params(self): + return urlparse.parse_qs(urlparse.urlparse(self.path).query) + def do_GET(self): pattern_handlers = { + "/api/v1/users/(?P\\d+)/active_threads$": self.do_user_profile, "/api/v1/users/(?P\\d+)$": self.do_user, "/api/v1/threads$": self.do_threads, "/api/v1/threads/(?P\\w+)$": self.do_thread, @@ -34,12 +40,34 @@ class StubCommentsServiceHandler(StubHttpRequestHandler): self.send_json_response({}) def do_user(self, user_id): - self.send_json_response({ + response = { "id": user_id, "upvoted_ids": [], "downvoted_ids": [], "subscribed_thread_ids": [], - }) + } + if 'course_id' in self._params: + response.update({ + "threads_count": 1, + "comments_count": 2 + }) + self.send_json_response(response) + + def do_user_profile(self, user_id): + if 'active_threads' in self.server.config: + user_threads = self.server.config['active_threads'][:] + params = self._params + page = int(params.get("page", ["1"])[0]) + per_page = int(params.get("per_page", ["20"])[0]) + num_pages = max(len(user_threads) - 1, 1) / per_page + 1 + user_threads = user_threads[(page - 1) * per_page:page * per_page] + self.send_json_response({ + "collection": user_threads, + "page": page, + "num_pages": num_pages + }) + else: + self.send_response(404, content="404 Not Found") def do_thread(self, thread_id): if thread_id in self.server.config.get('threads', {}): diff --git a/common/test/acceptance/fixtures/discussion.py b/common/test/acceptance/fixtures/discussion.py index e9104f199c..4d3061a15c 100644 --- a/common/test/acceptance/fixtures/discussion.py +++ b/common/test/acceptance/fixtures/discussion.py @@ -87,3 +87,19 @@ class SingleThreadViewFixture(object): "comments": json.dumps(self._get_comment_map()) } ) + +class UserProfileViewFixture(object): + + def __init__(self, threads): + self.threads = threads + + def push(self): + """ + Push the data to the stub comments service. + """ + requests.put( + '{}/set_config'.format(COMMENTS_STUB_URL), + data={ + "active_threads": json.dumps(self.threads), + } + ) diff --git a/common/test/acceptance/pages/lms/discussion.py b/common/test/acceptance/pages/lms/discussion.py index 5652ccfcca..2ec2894131 100644 --- a/common/test/acceptance/pages/lms/discussion.py +++ b/common/test/acceptance/pages/lms/discussion.py @@ -231,3 +231,76 @@ class InlineDiscussionThreadPage(DiscussionThreadPage): "Thread expanded" ).fulfill() + +class DiscussionUserProfilePage(CoursePage): + + TEXT_NEXT = u'Next >' + TEXT_PREV = u'< Previous' + PAGING_SELECTOR = "a.discussion-pagination[data-page-number]" + + def __init__(self, browser, course_id, user_id, username, page=1): + super(DiscussionUserProfilePage, self).__init__(browser, course_id) + self.url_path = "discussion/forum/dummy/users/{}?page={}".format(user_id, page) + self.username = username + + def is_browser_on_page(self): + return ( + self.q(css='section.discussion-user-threads[data-course-id="{}"]'.format(self.course_id)).present + and + self.q(css='section.user-profile div.sidebar-username').present + and + self.q(css='section.user-profile div.sidebar-username').text[0] == self.username + ) + + def get_shown_thread_ids(self): + elems = self.q(css="article.discussion-thread") + return [elem.get_attribute("id")[7:] for elem in elems] + + def get_current_page(self): + return int(self.q(css="nav.discussion-paginator li.current-page").text[0]) + + def _check_pager(self, text, page_number=None): + """ + returns True if 'text' matches the text in any of the pagination elements. If + page_number is provided, only return True if the element points to that result + page. + """ + elems = self.q(css=self.PAGING_SELECTOR).filter(lambda elem: elem.text == text) + if page_number: + elems = elems.filter(lambda elem: int(elem.get_attribute('data-page-number')) == page_number) + return elems.present + + def get_clickable_pages(self): + return sorted([ + int(elem.get_attribute('data-page-number')) + for elem in self.q(css=self.PAGING_SELECTOR) + if str(elem.text).isdigit() + ]) + + def is_prev_button_shown(self, page_number=None): + return self._check_pager(self.TEXT_PREV, page_number) + + def is_next_button_shown(self, page_number=None): + return self._check_pager(self.TEXT_NEXT, page_number) + + def _click_pager_with_text(self, text, page_number): + """ + click the first pagination element with whose text is `text` and ensure + the resulting page number matches `page_number`. + """ + targets = [elem for elem in self.q(css=self.PAGING_SELECTOR) if elem.text == text] + targets[0].click() + EmptyPromise( + lambda: self.get_current_page() == page_number, + "navigated to desired page" + ).fulfill() + + def click_prev_page(self): + self._click_pager_with_text(self.TEXT_PREV, self.get_current_page() - 1) + + def click_next_page(self): + self._click_pager_with_text(self.TEXT_NEXT, self.get_current_page() + 1) + + def click_on_page(self, page_number): + self._click_pager_with_text(unicode(page_number), page_number) + diff --git a/common/test/acceptance/tests/test_discussion.py b/common/test/acceptance/tests/test_discussion.py index cad4779bb2..49813948ba 100644 --- a/common/test/acceptance/tests/test_discussion.py +++ b/common/test/acceptance/tests/test_discussion.py @@ -10,10 +10,11 @@ from ..pages.lms.courseware import CoursewarePage from ..pages.lms.discussion import ( DiscussionTabSingleThreadPage, InlineDiscussionPage, - InlineDiscussionThreadPage + InlineDiscussionThreadPage, + DiscussionUserProfilePage ) from ..fixtures.course import CourseFixture, XBlockFixtureDesc -from ..fixtures.discussion import SingleThreadViewFixture, Thread, Response, Comment +from ..fixtures.discussion import SingleThreadViewFixture, UserProfileViewFixture, Thread, Response, Comment class DiscussionResponsePaginationTestMixin(object): @@ -301,3 +302,106 @@ class InlineDiscussionTest(UniqueCourseTest, DiscussionResponsePaginationTestMix def test_expand_discussion_empty(self): self.discussion_page.expand_discussion() self.assertEqual(self.discussion_page.get_num_displayed_threads(), 0) + + +class DiscussionUserProfileTest(UniqueCourseTest): + """ + Tests for user profile page in discussion tab. + """ + + PAGE_SIZE = 20 # django_comment_client.forum.views.THREADS_PER_PAGE + PROFILED_USERNAME = "profiled-user" + + def setUp(self): + super(DiscussionUserProfileTest, self).setUp() + CourseFixture(**self.course_info).install() + # The following line creates a user enrolled in our course, whose + # threads will be viewed, but not the one who will view the page. + # It isn't necessary to log them in, but using the AutoAuthPage + # saves a lot of code. + self.profiled_user_id = AutoAuthPage( + self.browser, + username=self.PROFILED_USERNAME, + course_id=self.course_id + ).visit().get_user_id() + # now create a second user who will view the profile. + self.user_id = AutoAuthPage( + self.browser, + course_id=self.course_id + ).visit().get_user_id() + + def check_pages(self, num_threads): + # set up the stub server to return the desired amount of thread results + threads = [Thread(id=uuid4().hex) for _ in range(num_threads)] + UserProfileViewFixture(threads).push() + # navigate to default view (page 1) + page = DiscussionUserProfilePage( + self.browser, + self.course_id, + self.profiled_user_id, + self.PROFILED_USERNAME + ) + page.visit() + + current_page = 1 + total_pages = max(num_threads - 1, 1) / self.PAGE_SIZE + 1 + all_pages = range(1, total_pages + 1) + + def _check_page(): + # ensure the page being displayed as "current" is the expected one + self.assertEqual(page.get_current_page(), current_page) + # ensure the expected threads are being shown in the right order + threads_expected = threads[(current_page - 1) * self.PAGE_SIZE:current_page * self.PAGE_SIZE] + self.assertEqual(page.get_shown_thread_ids(), [t["id"] for t in threads_expected]) + # ensure the clickable page numbers are the expected ones + self.assertEqual(page.get_clickable_pages(), [ + p for p in all_pages + if p != current_page + and p - 2 <= current_page <= p + 2 + or (current_page > 2 and p == 1) + or (current_page < total_pages and p == total_pages) + ]) + # ensure the previous button is shown, but only if it should be. + # when it is shown, make sure it works. + if current_page > 1: + self.assertTrue(page.is_prev_button_shown(current_page - 1)) + page.click_prev_page() + self.assertEqual(page.get_current_page(), current_page - 1) + page.click_next_page() + self.assertEqual(page.get_current_page(), current_page) + else: + self.assertFalse(page.is_prev_button_shown()) + # ensure the next button is shown, but only if it should be. + if current_page < total_pages: + self.assertTrue(page.is_next_button_shown(current_page + 1)) + else: + self.assertFalse(page.is_next_button_shown()) + + # click all the way up through each page + for i in range(current_page, total_pages): + _check_page() + if current_page < total_pages: + page.click_on_page(current_page + 1) + current_page += 1 + + # click all the way back down + for i in range(current_page, 0, -1): + _check_page() + if current_page > 1: + page.click_on_page(current_page - 1) + current_page -= 1 + + def test_0_threads(self): + self.check_pages(0) + + def test_1_thread(self): + self.check_pages(1) + + def test_20_threads(self): + self.check_pages(20) + + def test_21_threads(self): + self.check_pages(21) + + def test_151_threads(self): + self.check_pages(151)