Commit b1e5bbb8 by Greg Price

Merge pull request #2504 from edx/gprice/forum-pagination-acceptance

Add acceptance tests for forum response pagination
parents ff910028 8e3a77fa
"""
Stub implementation of cs_comments_service for acceptance tests
"""
from datetime import datetime
import re
import urlparse
from .http import StubHttpRequestHandler, StubHttpService
class StubCommentsServiceHandler(StubHttpRequestHandler):
def do_GET(self):
pattern_handlers = {
"/api/v1/users/(?P<user_id>\\d+)$": self.do_user,
"/api/v1/threads$": self.do_threads,
"/api/v1/threads/(?P<thread_id>\\w+)$": self.do_thread,
}
path = urlparse.urlparse(self.path).path
for pattern in pattern_handlers:
match = re.match(pattern, path)
if match:
pattern_handlers[pattern](**match.groupdict())
return
self.send_response(404, content="404 Not Found")
def do_PUT(self):
self.send_response(204, "")
def do_user(self, user_id):
self.send_json_response({
"id": user_id,
"upvoted_ids": [],
"downvoted_ids": [],
"subscribed_thread_ids": [],
})
def do_thread(self, thread_id):
match = re.search("(?P<num>\\d+)_responses", thread_id)
resp_total = int(match.group("num")) if match else 0
thread = {
"id": thread_id,
"commentable_id": "dummy",
"type": "thread",
"title": "Thread title",
"body": "Thread body",
"created_at": datetime.utcnow().isoformat(),
"unread_comments_count": 0,
"comments_count": resp_total,
"votes": {"up_count": 0},
"abuse_flaggers": [],
"closed": "closed" in thread_id,
}
params = urlparse.parse_qs(urlparse.urlparse(self.path).query)
if "recursive" in params and params["recursive"][0] == "True":
thread["resp_total"] = resp_total
thread["children"] = []
resp_skip = int(params.get("resp_skip", ["0"])[0])
resp_limit = int(params.get("resp_limit", ["10000"])[0])
num_responses = min(resp_limit, resp_total - resp_skip)
self.log_message("Generating {} children; resp_limit={} resp_total={} resp_skip={}".format(num_responses, resp_limit, resp_total, resp_skip))
for i in range(num_responses):
response_id = str(resp_skip + i)
thread["children"].append({
"id": str(response_id),
"type": "comment",
"body": response_id,
"created_at": datetime.utcnow().isoformat(),
"votes": {"up_count": 0},
"abuse_flaggers": [],
})
self.send_json_response(thread)
def do_threads(self):
self.send_json_response({"collection": [], "page": 1, "num_pages": 1})
class StubCommentsService(StubHttpService):
HANDLER_CLASS = StubCommentsServiceHandler
...@@ -202,6 +202,13 @@ class StubHttpRequestHandler(BaseHTTPRequestHandler, object): ...@@ -202,6 +202,13 @@ class StubHttpRequestHandler(BaseHTTPRequestHandler, object):
if content is not None: if content is not None:
self.wfile.write(content) self.wfile.write(content)
def send_json_response(self, content):
"""
Send a response with status code 200, the given content serialized as
JSON, and the Content-Type header set appropriately
"""
self.send_response(200, json.dumps(content), {"Content-Type": "application/json"})
def _format_msg(self, format_str, *args): def _format_msg(self, format_str, *args):
""" """
Format message for logging. Format message for logging.
......
...@@ -4,6 +4,7 @@ Command-line utility to start a stub service. ...@@ -4,6 +4,7 @@ Command-line utility to start a stub service.
import sys import sys
import time import time
import logging import logging
from .comments import StubCommentsService
from .xqueue import StubXQueueService from .xqueue import StubXQueueService
from .youtube import StubYouTubeService from .youtube import StubYouTubeService
from .ora import StubOraService from .ora import StubOraService
...@@ -14,7 +15,8 @@ USAGE = "USAGE: python -m stubs.start SERVICE_NAME PORT_NUM [CONFIG_KEY=CONFIG_V ...@@ -14,7 +15,8 @@ USAGE = "USAGE: python -m stubs.start SERVICE_NAME PORT_NUM [CONFIG_KEY=CONFIG_V
SERVICES = { SERVICES = {
'xqueue': StubXQueueService, 'xqueue': StubXQueueService,
'youtube': StubYouTubeService, 'youtube': StubYouTubeService,
'ora': StubOraService 'ora': StubOraService,
'comments': StubCommentsService,
} }
# Log to stdout, including debug messages # Log to stdout, including debug messages
......
...@@ -11,7 +11,7 @@ class CourseAboutPage(CoursePage): ...@@ -11,7 +11,7 @@ class CourseAboutPage(CoursePage):
Course about page (with registration button) Course about page (with registration button)
""" """
URL_PATH = "about" url_path = "about"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('section.course-info') return self.is_css_present('section.course-info')
......
...@@ -10,7 +10,7 @@ class CourseInfoPage(CoursePage): ...@@ -10,7 +10,7 @@ class CourseInfoPage(CoursePage):
Course info. Course info.
""" """
URL_PATH = "info" url_path = "info"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('section.updates') return self.is_css_present('section.updates')
......
...@@ -13,7 +13,7 @@ class CoursePage(PageObject): ...@@ -13,7 +13,7 @@ class CoursePage(PageObject):
# Overridden by subclasses to provide the relative path within the course # Overridden by subclasses to provide the relative path within the course
# Paths should not include the leading forward slash. # Paths should not include the leading forward slash.
URL_PATH = "" url_path = ""
def __init__(self, browser, course_id): def __init__(self, browser, course_id):
""" """
...@@ -28,4 +28,4 @@ class CoursePage(PageObject): ...@@ -28,4 +28,4 @@ class CoursePage(PageObject):
""" """
Construct a URL to the page within the course. Construct a URL to the page within the course.
""" """
return BASE_URL + "/courses/" + self.course_id + "/" + self.URL_PATH return BASE_URL + "/courses/" + self.course_id + "/" + self.url_path
from bok_choy.page_object import unguarded
from bok_choy.promise import EmptyPromise, fulfill
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
def is_browser_on_page(self):
return self.is_css_present(
"body.discussion .discussion-article[data-id='{thread_id}']".format(thread_id=self.thread_id)
)
@property
@unguarded
def url_path(self):
return "discussion/forum/dummy/threads/" + self.thread_id
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.css_text(selector)
return text_list[0] if text_list else None
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 self.css_count(".discussion-response")
def get_shown_responses_text(self):
"""Returns the shown response count text, or None if not present"""
return self._get_element_text(".response-display-count")
def get_load_responses_button_text(self):
"""Returns the load more responses button text, or None if not present"""
return self._get_element_text(".load-response-button")
def load_more_responses(self):
"""Clicks the laod more responses button and waits for responses to load"""
self.css_click(".load-response-button")
fulfill(EmptyPromise(
lambda: not self.is_css_present(".loading"),
"Loading more responses completed"
))
def has_add_response_button(self):
"""Returns true if the add response button is visible, false otherwise"""
return (
self.is_css_present(".add-response-btn") and
self.css_map(".add-response-btn", lambda el: el.visible)[0]
)
def click_add_response_button(self):
"""
Clicks the add response button and ensures that the response text
field receives focus
"""
self.css_click(".add-response-btn")
fulfill(EmptyPromise(
lambda: self.is_css_present("#wmd-input-reply-body-{thread_id}:focus".format(thread_id=self.thread_id)),
"Response field received focus"
))
...@@ -10,7 +10,7 @@ class ProgressPage(CoursePage): ...@@ -10,7 +10,7 @@ class ProgressPage(CoursePage):
Student progress page. Student progress page.
""" """
URL_PATH = "progress" url_path = "progress"
def is_browser_on_page(self): def is_browser_on_page(self):
has_course_info = self.is_css_present('div.course-info') has_course_info = self.is_css_present('div.course-info')
......
...@@ -10,7 +10,7 @@ class AssetIndexPage(CoursePage): ...@@ -10,7 +10,7 @@ class AssetIndexPage(CoursePage):
The Files and Uploads page for a course in Studio The Files and Uploads page for a course in Studio
""" """
URL_PATH = "assets" url_path = "assets"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-uploads') return self.is_css_present('body.view-uploads')
...@@ -10,7 +10,7 @@ class ChecklistsPage(CoursePage): ...@@ -10,7 +10,7 @@ class ChecklistsPage(CoursePage):
Course Checklists page. Course Checklists page.
""" """
URL_PATH = "checklists" url_path = "checklists"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-checklists') return self.is_css_present('body.view-checklists')
...@@ -10,7 +10,7 @@ class ImportPage(CoursePage): ...@@ -10,7 +10,7 @@ class ImportPage(CoursePage):
Course Import page. Course Import page.
""" """
URL_PATH = "import" url_path = "import"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-import') return self.is_css_present('body.view-import')
...@@ -10,7 +10,7 @@ class CourseUpdatesPage(CoursePage): ...@@ -10,7 +10,7 @@ class CourseUpdatesPage(CoursePage):
Course Updates page. Course Updates page.
""" """
URL_PATH = "course_info" url_path = "course_info"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-updates') return self.is_css_present('body.view-updates')
...@@ -13,7 +13,7 @@ class CoursePage(PageObject): ...@@ -13,7 +13,7 @@ class CoursePage(PageObject):
# Overridden by subclasses to provide the relative path within the course # Overridden by subclasses to provide the relative path within the course
# Does not need to include the leading forward or trailing slash # Does not need to include the leading forward or trailing slash
URL_PATH = "" url_path = ""
def __init__(self, browser, course_org, course_num, course_run): def __init__(self, browser, course_org, course_num, course_run):
""" """
...@@ -35,7 +35,7 @@ class CoursePage(PageObject): ...@@ -35,7 +35,7 @@ class CoursePage(PageObject):
Construct a URL to the page within the course. Construct a URL to the page within the course.
""" """
return "/".join([ return "/".join([
BASE_URL, self.URL_PATH, BASE_URL, self.url_path,
"{course_org}.{course_num}.{course_run}".format(**self.course_info), "{course_org}.{course_num}.{course_run}".format(**self.course_info),
"branch", "draft", "block", self.course_info['course_run'] "branch", "draft", "block", self.course_info['course_run']
]) ])
...@@ -10,7 +10,7 @@ class StaticPagesPage(CoursePage): ...@@ -10,7 +10,7 @@ class StaticPagesPage(CoursePage):
Static Pages page for a course. Static Pages page for a course.
""" """
URL_PATH = "tabs" url_path = "tabs"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-static-pages') return self.is_css_present('body.view-static-pages')
...@@ -10,7 +10,7 @@ class ExportPage(CoursePage): ...@@ -10,7 +10,7 @@ class ExportPage(CoursePage):
Course Export page. Course Export page.
""" """
URL_PATH = "export" url_path = "export"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-export') return self.is_css_present('body.view-export')
...@@ -10,7 +10,7 @@ class CourseTeamPage(CoursePage): ...@@ -10,7 +10,7 @@ class CourseTeamPage(CoursePage):
Course Team page in Studio. Course Team page in Studio.
""" """
URL_PATH = "course_team" url_path = "course_team"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-team') return self.is_css_present('body.view-team')
...@@ -10,7 +10,7 @@ class CourseOutlinePage(CoursePage): ...@@ -10,7 +10,7 @@ class CourseOutlinePage(CoursePage):
Course Outline page in Studio. Course Outline page in Studio.
""" """
URL_PATH = "course" url_path = "course"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-outline') return self.is_css_present('body.view-outline')
...@@ -10,7 +10,7 @@ class SettingsPage(CoursePage): ...@@ -10,7 +10,7 @@ class SettingsPage(CoursePage):
Course Schedule and Details Settings page. Course Schedule and Details Settings page.
""" """
URL_PATH = "settings/details" url_path = "settings/details"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-settings') return self.is_css_present('body.view-settings')
...@@ -10,7 +10,7 @@ class AdvancedSettingsPage(CoursePage): ...@@ -10,7 +10,7 @@ class AdvancedSettingsPage(CoursePage):
Course Advanced Settings page. Course Advanced Settings page.
""" """
URL_PATH = "settings/advanced" url_path = "settings/advanced"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.advanced') return self.is_css_present('body.advanced')
...@@ -10,7 +10,7 @@ class GradingPage(CoursePage): ...@@ -10,7 +10,7 @@ class GradingPage(CoursePage):
Course Grading Settings page. Course Grading Settings page.
""" """
URL_PATH = "settings/grading" url_path = "settings/grading"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.grading') return self.is_css_present('body.grading')
...@@ -10,7 +10,7 @@ class TextbooksPage(CoursePage): ...@@ -10,7 +10,7 @@ class TextbooksPage(CoursePage):
Course Textbooks page. Course Textbooks page.
""" """
URL_PATH = "textbooks" url_path = "textbooks"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-textbooks') return self.is_css_present('body.view-textbooks')
"""
Tests for discussion pages
"""
from .helpers import UniqueCourseTest
from ..pages.studio.auto_auth import AutoAuthPage
from ..pages.lms.discussion_single_thread import DiscussionSingleThreadPage
from ..fixtures.course import CourseFixture
class DiscussionSingleThreadTest(UniqueCourseTest):
"""
Tests for the discussion page displaying a single thread
"""
def setUp(self):
super(DiscussionSingleThreadTest, self).setUp()
# Create a course to register for
CourseFixture(**self.course_info).install()
AutoAuthPage(self.browser, course_id=self.course_id).visit()
def test_no_responses(self):
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):
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):
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):
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")
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 test_add_response_button(self):
page = DiscussionSingleThreadPage(self.browser, self.course_id, "5_responses")
page.visit()
self.assertTrue(page.has_add_response_button())
page.click_add_response_button()
def test_add_response_button_closed_thread(self):
page = DiscussionSingleThreadPage(self.browser, self.course_id, "5_responses_closed")
page.visit()
self.assertFalse(page.has_add_response_button())
...@@ -41,6 +41,11 @@ BOK_CHOY_STUBS = { ...@@ -41,6 +41,11 @@ BOK_CHOY_STUBS = {
:port => 8041, :port => 8041,
:log => File.join(BOK_CHOY_LOG_DIR, "bok_choy_ora.log"), :log => File.join(BOK_CHOY_LOG_DIR, "bok_choy_ora.log"),
:config => '' :config => ''
},
:comments => {
:port => 4567,
:log => File.join(BOK_CHOY_LOG_DIR, "bok_choy_comments.log")
} }
} }
......
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