Commit 16847d85 by Greg Price

Add bok-choy tests for inline response pagination

parent 3af90ef2
......@@ -14,6 +14,7 @@ class StubCommentsServiceHandler(StubHttpRequestHandler):
"/api/v1/threads$": self.do_threads,
"/api/v1/threads/(?P<thread_id>\\w+)$": self.do_thread,
"/api/v1/comments/(?P<comment_id>\\w+)$": self.do_comment,
"/api/v1/(?P<commentable_id>\\w+)/threads$": self.do_commentable,
}
path = urlparse.urlparse(self.path).path
for pattern in pattern_handlers:
......@@ -63,6 +64,17 @@ class StubCommentsServiceHandler(StubHttpRequestHandler):
comment = self.server.config['comments'][comment_id]
self.send_json_response(comment)
def do_commentable(self, commentable_id):
self.send_json_response({
"collection": [
thread
for thread in self.server.config.get('threads', {}).values()
if thread.get('commentable_id') == commentable_id
],
"page": 1,
"num_pages": 1,
})
class StubCommentsService(StubHttpService):
HANDLER_CLASS = StubCommentsServiceHandler
"""
Courseware page.
"""
from .course_page import CoursePage
class CoursewarePage(CoursePage):
"""
Course info.
"""
url_path = "courseware"
def is_browser_on_page(self):
return self.q(css='body.courseware').present
from bok_choy.page_object import unguarded
from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise
from .course_page import CoursePage
class DiscussionSingleThreadPage(CoursePage):
def __init__(self, browser, course_id, thread_id):
super(DiscussionSingleThreadPage, self).__init__(browser, course_id)
self.thread_id = thread_id
class DiscussionThreadPage(PageObject):
url = None
def is_browser_on_page(self):
return self.q(
css="body.discussion .discussion-article[data-id='{thread_id}']".format(thread_id=self.thread_id)
).present
def __init__(self, browser, thread_selector):
super(DiscussionThreadPage, self).__init__(browser)
self.thread_selector = thread_selector
def _find_within(self, selector):
"""
Returns a query corresponding to the given CSS selector within the scope
of this thread page
"""
return self.q(css=self.thread_selector + " " + selector)
@property
@unguarded
def url_path(self):
return "discussion/forum/dummy/threads/" + self.thread_id
def is_browser_on_page(self):
return self.q(css=self.thread_selector).present
def _get_element_text(self, selector):
"""
Returns the text of the first element matching the given selector, or
None if no such element exists
"""
text_list = self.q(css=selector).text
text_list = self._find_within(selector).text
return text_list[0] if text_list else None
def _is_element_visible(self, selector):
query = self._find_within(selector)
return query.present and query.visible
def get_response_total_text(self):
"""Returns the response count text, or None if not present"""
return self._get_element_text(".response-count")
def get_num_displayed_responses(self):
"""Returns the number of responses actually rendered"""
return len(self.q(css=".discussion-response").results)
return len(self._find_within(".discussion-response"))
def get_shown_responses_text(self):
"""Returns the shown response count text, or None if not present"""
......@@ -45,7 +51,7 @@ class DiscussionSingleThreadPage(CoursePage):
def load_more_responses(self):
"""Clicks the load more responses button and waits for responses to load"""
self.q(css=".load-response-button").first.click()
self._find_within(".load-response-button").click()
def _is_ajax_finished():
return self.browser.execute_script("return jQuery.active") == 0
......@@ -64,25 +70,19 @@ class DiscussionSingleThreadPage(CoursePage):
Clicks the add response button and ensures that the response text
field receives focus
"""
self.q(css=".add-response-btn").first.click()
self._find_within(".add-response-btn").first.click()
EmptyPromise(
lambda: self.q(css="#wmd-input-reply-body-{thread_id}:focus".format(thread_id=self.thread_id)),
lambda: self._find_within(".discussion-reply-new textarea:focus").present,
"Response field received focus"
).fulfill()
def _is_element_visible(self, selector):
return (
self.q(css=selector).present and
self.q(css=selector).visible
)
def is_response_editor_visible(self, response_id):
"""Returns true if the response editor is present, false otherwise"""
return self._is_element_visible(".response_{} .edit-post-body".format(response_id))
def start_response_edit(self, response_id):
"""Click the edit button for the response, loading the editing view"""
self.q(css=".response_{} .discussion-response .action-edit".format(response_id)).first.click()
self._find_within(".response_{} .discussion-response .action-edit".format(response_id)).first.click()
EmptyPromise(
lambda: self.is_response_editor_visible(response_id),
"Response edit started"
......@@ -105,7 +105,7 @@ class DiscussionSingleThreadPage(CoursePage):
def delete_comment(self, comment_id):
with self.handle_alert():
self.q(css="#comment_{} div.action-delete".format(comment_id)).first.click()
self._find_within("#comment_{} div.action-delete".format(comment_id)).first.click()
EmptyPromise(
lambda: not self.is_comment_visible(comment_id),
"Deleted comment was removed"
......@@ -120,12 +120,12 @@ class DiscussionSingleThreadPage(CoursePage):
return self._is_element_visible(".edit-comment-body[data-id='{}']".format(comment_id))
def _get_comment_editor_value(self, comment_id):
return self.q(css="#wmd-input-edit-comment-body-{}".format(comment_id)).text[0]
return self._find_within("#wmd-input-edit-comment-body-{}".format(comment_id)).text[0]
def start_comment_edit(self, comment_id):
"""Click the edit button for the comment, loading the editing view"""
old_body = self.get_comment_body(comment_id)
self.q(css="#comment_{} .action-edit".format(comment_id)).first.click()
self._find_within("#comment_{} .action-edit".format(comment_id)).first.click()
EmptyPromise(
lambda: (
self.is_comment_editor_visible(comment_id) and
......@@ -137,11 +137,11 @@ class DiscussionSingleThreadPage(CoursePage):
def set_comment_editor_value(self, comment_id, new_body):
"""Replace the contents of the comment editor"""
self.q(css="#comment_{} .wmd-input".format(comment_id)).fill(new_body)
self._find_within("#comment_{} .wmd-input".format(comment_id)).fill(new_body)
def submit_comment_edit(self, comment_id, new_comment_body):
"""Click the submit button on the comment editor"""
self.q(css="#comment_{} .post-update".format(comment_id)).first.click()
self._find_within("#comment_{} .post-update".format(comment_id)).first.click()
EmptyPromise(
lambda: (
not self.is_comment_editor_visible(comment_id) and
......@@ -153,7 +153,7 @@ class DiscussionSingleThreadPage(CoursePage):
def cancel_comment_edit(self, comment_id, original_body):
"""Click the cancel button on the comment editor"""
self.q(css="#comment_{} .post-cancel".format(comment_id)).first.click()
self._find_within("#comment_{} .post-cancel".format(comment_id)).first.click()
EmptyPromise(
lambda: (
not self.is_comment_editor_visible(comment_id) and
......@@ -162,3 +162,72 @@ class DiscussionSingleThreadPage(CoursePage):
),
"Comment edit was canceled"
).fulfill()
class DiscussionTabSingleThreadPage(CoursePage):
def __init__(self, browser, course_id, thread_id):
super(DiscussionTabSingleThreadPage, self).__init__(browser, course_id)
self.thread_page = DiscussionThreadPage(
browser,
"body.discussion .discussion-article[data-id='{thread_id}']".format(thread_id=thread_id)
)
self.url_path = "discussion/forum/dummy/threads/" + thread_id
def is_browser_on_page(self):
return self.thread_page.is_browser_on_page()
def __getattr__(self, name):
return getattr(self.thread_page, name)
class InlineDiscussionPage(PageObject):
url = None
def __init__(self, browser, discussion_id):
super(InlineDiscussionPage, self).__init__(browser)
self._discussion_selector = (
"body.courseware .discussion-module[data-discussion-id='{discussion_id}'] ".format(
discussion_id=discussion_id
)
)
def _find_within(self, selector):
"""
Returns a query corresponding to the given CSS selector within the scope
of this discussion page
"""
return self.q(css=self._discussion_selector + " " + selector)
def is_browser_on_page(self):
return self.q(css=self._discussion_selector).present
def is_discussion_expanded(self):
return self._find_within(".discussion").present
def expand_discussion(self):
"""Click the link to expand the discussion"""
self._find_within(".discussion-show").first.click()
EmptyPromise(
self.is_discussion_expanded,
"Discussion expanded"
).fulfill()
def get_num_displayed_threads(self):
return len(self._find_within(".discussion-thread"))
class InlineDiscussionThreadPage(DiscussionThreadPage):
def __init__(self, browser, thread_id):
super(InlineDiscussionThreadPage, self).__init__(
browser,
"body.courseware .discussion-module #thread_{thread_id}".format(thread_id=thread_id)
)
def expand(self):
"""Clicks the link to expand the thread"""
self._find_within(".expand-post").first.click()
EmptyPromise(
lambda: bool(self.get_response_total_text()),
"Thread expanded"
).fulfill()
......@@ -2,96 +2,132 @@
Tests for discussion pages
"""
from uuid import uuid4
from .helpers import UniqueCourseTest
from ..pages.studio.auto_auth import AutoAuthPage
from ..pages.lms.discussion_single_thread import DiscussionSingleThreadPage
from ..fixtures.course import CourseFixture
from ..pages.lms.courseware import CoursewarePage
from ..pages.lms.discussion import (
DiscussionTabSingleThreadPage,
InlineDiscussionPage,
InlineDiscussionThreadPage
)
from ..fixtures.course import CourseFixture, XBlockFixtureDesc
from ..fixtures.discussion import SingleThreadViewFixture, Thread, Response, Comment
class DiscussionSingleThreadTest(UniqueCourseTest):
class DiscussionResponsePaginationTestMixin(object):
"""
Tests for the discussion page displaying a single thread
A mixin containing tests for response pagination for use by both inline
discussion and the discussion tab
"""
def setUp(self):
super(DiscussionSingleThreadTest, self).setUp()
def setup_thread(self, num_responses, **thread_kwargs):
"""
Create a test thread with the given number of responses, passing all
keyword arguments through to the Thread fixture, then invoke
setup_thread_page.
"""
thread_id = "test_thread_{}".format(uuid4().hex)
thread_fixture = SingleThreadViewFixture(
Thread(id=thread_id, commentable_id=self.discussion_id, **thread_kwargs)
)
for i in range(num_responses):
thread_fixture.addResponse(Response(id=str(i), body=str(i)))
thread_fixture.push()
self.setup_thread_page(thread_id)
# Create a course to register for
CourseFixture(**self.course_info).install()
def assert_response_display_correct(self, response_total, displayed_responses):
"""
Assert that various aspects of the display of responses are all correct:
* Text indicating total number of responses
* Presence of "Add a response" button
* Number of responses actually displayed
* Presence and text of indicator of how many responses are shown
* Presence and text of button to load more responses
"""
self.assertEqual(
self.thread_page.get_response_total_text(),
str(response_total) + " responses"
)
self.assertEqual(self.thread_page.has_add_response_button(), response_total != 0)
self.assertEqual(self.thread_page.get_num_displayed_responses(), displayed_responses)
self.assertEqual(
self.thread_page.get_shown_responses_text(),
(
None if response_total == 0 else
"Showing all responses" if response_total == displayed_responses else
"Showing first {} responses".format(displayed_responses)
)
)
self.assertEqual(
self.thread_page.get_load_responses_button_text(),
(
None if response_total == displayed_responses else
"Load all responses" if response_total - displayed_responses < 100 else
"Load next 100 responses"
)
)
def test_pagination_no_responses(self):
self.setup_thread(0)
self.assert_response_display_correct(0, 0)
def test_pagination_few_responses(self):
self.setup_thread(5)
self.assert_response_display_correct(5, 5)
def test_pagination_two_response_pages(self):
self.setup_thread(50)
self.assert_response_display_correct(50, 25)
self.thread_page.load_more_responses()
self.assert_response_display_correct(50, 50)
def test_pagination_exactly_two_response_pages(self):
self.setup_thread(125)
self.assert_response_display_correct(125, 25)
self.thread_page.load_more_responses()
self.assert_response_display_correct(125, 125)
def test_pagination_three_response_pages(self):
self.setup_thread(150)
self.assert_response_display_correct(150, 25)
self.thread_page.load_more_responses()
self.assert_response_display_correct(150, 125)
self.thread_page.load_more_responses()
self.assert_response_display_correct(150, 150)
self.user_id = AutoAuthPage(self.browser, course_id=self.course_id).visit().get_user_id()
def test_add_response_button(self):
self.setup_thread(5)
self.assertTrue(self.thread_page.has_add_response_button())
self.thread_page.click_add_response_button()
def setup_thread(self, thread, num_responses):
view = SingleThreadViewFixture(thread=thread)
for i in range(num_responses):
view.addResponse(Response(id=str(i), body=str(i)))
view.push()
def test_add_response_button_closed_thread(self):
self.setup_thread(5, closed=True)
self.assertFalse(self.thread_page.has_add_response_button())
def test_no_responses(self):
self.setup_thread(Thread(id="0_responses"), 0)
page = DiscussionSingleThreadPage(self.browser, self.course_id, "0_responses")
page.visit()
self.assertEqual(page.get_response_total_text(), "0 responses")
self.assertFalse(page.has_add_response_button())
self.assertEqual(page.get_num_displayed_responses(), 0)
self.assertEqual(page.get_shown_responses_text(), None)
self.assertIsNone(page.get_load_responses_button_text())
def test_few_responses(self):
self.setup_thread(Thread(id="5_responses"), 5)
page = DiscussionSingleThreadPage(self.browser, self.course_id, "5_responses")
page.visit()
self.assertEqual(page.get_response_total_text(), "5 responses")
self.assertEqual(page.get_num_displayed_responses(), 5)
self.assertEqual(page.get_shown_responses_text(), "Showing all responses")
self.assertIsNone(page.get_load_responses_button_text())
def test_two_response_pages(self):
self.setup_thread(Thread(id="50_responses"), 50)
page = DiscussionSingleThreadPage(self.browser, self.course_id, "50_responses")
page.visit()
self.assertEqual(page.get_response_total_text(), "50 responses")
self.assertEqual(page.get_num_displayed_responses(), 25)
self.assertEqual(page.get_shown_responses_text(), "Showing first 25 responses")
self.assertEqual(page.get_load_responses_button_text(), "Load all responses")
page.load_more_responses()
self.assertEqual(page.get_num_displayed_responses(), 50)
self.assertEqual(page.get_shown_responses_text(), "Showing all responses")
self.assertEqual(page.get_load_responses_button_text(), None)
def test_three_response_pages(self):
self.setup_thread(Thread(id="150_responses"), 150)
page = DiscussionSingleThreadPage(self.browser, self.course_id, "150_responses")
page.visit()
self.assertEqual(page.get_response_total_text(), "150 responses")
self.assertEqual(page.get_num_displayed_responses(), 25)
self.assertEqual(page.get_shown_responses_text(), "Showing first 25 responses")
self.assertEqual(page.get_load_responses_button_text(), "Load next 100 responses")
page.load_more_responses()
self.assertEqual(page.get_num_displayed_responses(), 125)
self.assertEqual(page.get_shown_responses_text(), "Showing first 125 responses")
self.assertEqual(page.get_load_responses_button_text(), "Load all responses")
class DiscussionTabSingleThreadTest(UniqueCourseTest, DiscussionResponsePaginationTestMixin):
"""
Tests for the discussion page displaying a single thread
"""
page.load_more_responses()
self.assertEqual(page.get_num_displayed_responses(), 150)
self.assertEqual(page.get_shown_responses_text(), "Showing all responses")
self.assertEqual(page.get_load_responses_button_text(), None)
def setUp(self):
super(DiscussionTabSingleThreadTest, self).setUp()
self.discussion_id = "test_discussion_{}".format(uuid4().hex)
def test_add_response_button(self):
self.setup_thread(Thread(id="5_responses"), 5)
page = DiscussionSingleThreadPage(self.browser, self.course_id, "5_responses")
page.visit()
self.assertTrue(page.has_add_response_button())
page.click_add_response_button()
# Create a course to register for
CourseFixture(**self.course_info).install()
def test_add_response_button_closed_thread(self):
self.setup_thread(Thread(id="5_responses_closed", closed=True), 5)
page = DiscussionSingleThreadPage(self.browser, self.course_id, "5_responses_closed")
page.visit()
self.assertFalse(page.has_add_response_button())
AutoAuthPage(self.browser, course_id=self.course_id).visit()
def setup_thread_page(self, thread_id):
self.thread_page = DiscussionTabSingleThreadPage(self.browser, self.course_id, thread_id) # pylint:disable=W0201
self.thread_page.visit()
class DiscussionCommentDeletionTest(UniqueCourseTest):
......@@ -119,7 +155,7 @@ class DiscussionCommentDeletionTest(UniqueCourseTest):
def test_comment_deletion_as_student(self):
self.setup_user()
self.setup_view()
page = DiscussionSingleThreadPage(self.browser, self.course_id, "comment_deletion_test_thread")
page = DiscussionTabSingleThreadPage(self.browser, self.course_id, "comment_deletion_test_thread")
page.visit()
self.assertTrue(page.is_comment_deletable("comment_self_author"))
self.assertTrue(page.is_comment_visible("comment_other_author"))
......@@ -129,7 +165,7 @@ class DiscussionCommentDeletionTest(UniqueCourseTest):
def test_comment_deletion_as_moderator(self):
self.setup_user(roles=['Moderator'])
self.setup_view()
page = DiscussionSingleThreadPage(self.browser, self.course_id, "comment_deletion_test_thread")
page = DiscussionTabSingleThreadPage(self.browser, self.course_id, "comment_deletion_test_thread")
page.visit()
self.assertTrue(page.is_comment_deletable("comment_self_author"))
self.assertTrue(page.is_comment_deletable("comment_other_author"))
......@@ -168,7 +204,7 @@ class DiscussionCommentEditTest(UniqueCourseTest):
def test_edit_comment_as_student(self):
self.setup_user()
self.setup_view()
page = DiscussionSingleThreadPage(self.browser, self.course_id, "comment_edit_test_thread")
page = DiscussionTabSingleThreadPage(self.browser, self.course_id, "comment_edit_test_thread")
page.visit()
self.assertTrue(page.is_comment_editable("comment_self_author"))
self.assertTrue(page.is_comment_visible("comment_other_author"))
......@@ -178,7 +214,7 @@ class DiscussionCommentEditTest(UniqueCourseTest):
def test_edit_comment_as_moderator(self):
self.setup_user(roles=["Moderator"])
self.setup_view()
page = DiscussionSingleThreadPage(self.browser, self.course_id, "comment_edit_test_thread")
page = DiscussionTabSingleThreadPage(self.browser, self.course_id, "comment_edit_test_thread")
page.visit()
self.assertTrue(page.is_comment_editable("comment_self_author"))
self.assertTrue(page.is_comment_editable("comment_other_author"))
......@@ -188,7 +224,7 @@ class DiscussionCommentEditTest(UniqueCourseTest):
def test_cancel_comment_edit(self):
self.setup_user()
self.setup_view()
page = DiscussionSingleThreadPage(self.browser, self.course_id, "comment_edit_test_thread")
page = DiscussionTabSingleThreadPage(self.browser, self.course_id, "comment_edit_test_thread")
page.visit()
self.assertTrue(page.is_comment_editable("comment_self_author"))
original_body = page.get_comment_body("comment_self_author")
......@@ -200,7 +236,7 @@ class DiscussionCommentEditTest(UniqueCourseTest):
"""Only one editor should be visible at a time within a single response"""
self.setup_user(roles=["Moderator"])
self.setup_view()
page = DiscussionSingleThreadPage(self.browser, self.course_id, "comment_edit_test_thread")
page = DiscussionTabSingleThreadPage(self.browser, self.course_id, "comment_edit_test_thread")
page.visit()
self.assertTrue(page.is_comment_editable("comment_self_author"))
self.assertTrue(page.is_comment_editable("comment_other_author"))
......@@ -224,3 +260,44 @@ class DiscussionCommentEditTest(UniqueCourseTest):
page.cancel_comment_edit("comment_self_author", original_body)
self.assertFalse(page.is_comment_editor_visible("comment_self_author"))
self.assertTrue(page.is_add_comment_visible("response1"))
class InlineDiscussionTest(UniqueCourseTest, DiscussionResponsePaginationTestMixin):
"""
Tests for inline discussions
"""
def setUp(self):
super(InlineDiscussionTest, self).setUp()
self.discussion_id = "test_discussion_{}".format(uuid4().hex)
CourseFixture(**self.course_info).add_children(
XBlockFixtureDesc("chapter", "Test Section").add_children(
XBlockFixtureDesc("sequential", "Test Subsection").add_children(
XBlockFixtureDesc("vertical", "Test Unit").add_children(
XBlockFixtureDesc(
"discussion",
"Test Discussion",
metadata={"discussion_id": self.discussion_id}
)
)
)
)
).install()
AutoAuthPage(self.browser, course_id=self.course_id).visit()
CoursewarePage(self.browser, self.course_id).visit()
self.discussion_page = InlineDiscussionPage(self.browser, self.discussion_id)
def setup_thread_page(self, thread_id):
self.discussion_page.expand_discussion()
self.assertEqual(self.discussion_page.get_num_displayed_threads(), 1)
self.thread_page = InlineDiscussionThreadPage(self.browser, thread_id) # pylint:disable=W0201
self.thread_page.expand()
def test_initial_render(self):
self.assertFalse(self.discussion_page.is_discussion_expanded())
def test_expand_discussion_empty(self):
self.discussion_page.expand_discussion()
self.assertEqual(self.discussion_page.get_num_displayed_threads(), 0)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment