Commit 806a6b9f by Jay Zoldak

Update edx-platform page objects and tests to be compatible with new bok-choy version

Change expectedFailure decorators to skip, because that still takes
a screenshot as if it were an error and at least one of those
take a super long time when running locally on devstack.
parent c086cbc3
...@@ -14,7 +14,8 @@ class CourseAboutPage(CoursePage): ...@@ -14,7 +14,8 @@ class CourseAboutPage(CoursePage):
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.q(css='section.course-info').present
def register(self): def register(self):
""" """
...@@ -22,7 +23,7 @@ class CourseAboutPage(CoursePage): ...@@ -22,7 +23,7 @@ class CourseAboutPage(CoursePage):
Waits for the registration page to load, then Waits for the registration page to load, then
returns the registration page object. returns the registration page object.
""" """
self.css_click('a.register') self.q(css='a.register').first.click()
registration_page = RegisterPage(self.browser, self.course_id) registration_page = RegisterPage(self.browser, self.course_id)
registration_page.wait_for_page() registration_page.wait_for_page()
......
...@@ -13,18 +13,18 @@ class CourseInfoPage(CoursePage): ...@@ -13,18 +13,18 @@ class CourseInfoPage(CoursePage):
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.q(css='section.updates').present
@property @property
def num_updates(self): def num_updates(self):
""" """
Return the number of updates on the page. Return the number of updates on the page.
""" """
return self.css_count('section.updates section article') return len(self.q(css='section.updates section article').results)
@property @property
def handout_links(self): def handout_links(self):
""" """
Return a list of handout assets links. Return a list of handout assets links.
""" """
return self.css_map('section.handouts ol li a', lambda el: el['href']) return self.q(css='section.handouts ol li a').map(lambda el: el.get_attribute('href')).results
...@@ -4,7 +4,7 @@ Course navigation page object ...@@ -4,7 +4,7 @@ Course navigation page object
import re import re
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise, fulfill_after from bok_choy.promise import EmptyPromise
class CourseNavPage(PageObject): class CourseNavPage(PageObject):
...@@ -15,7 +15,7 @@ class CourseNavPage(PageObject): ...@@ -15,7 +15,7 @@ class CourseNavPage(PageObject):
url = None url = None
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('div.course-index') return self.q(css='div.course-index').present
@property @property
def sections(self): def sections(self):
...@@ -38,9 +38,7 @@ class CourseNavPage(PageObject): ...@@ -38,9 +38,7 @@ class CourseNavPage(PageObject):
section_titles = self._section_titles() section_titles = self._section_titles()
# Get the section titles for each chapter # Get the section titles for each chapter
for sec_index in range(len(section_titles)): for sec_index, sec_title in enumerate(section_titles):
sec_title = section_titles[sec_index]
if len(section_titles) < 1: if len(section_titles) < 1:
self.warning("Could not find subsections for '{0}'".format(sec_title)) self.warning("Could not find subsections for '{0}'".format(sec_title))
...@@ -60,7 +58,7 @@ class CourseNavPage(PageObject): ...@@ -60,7 +58,7 @@ class CourseNavPage(PageObject):
['Chemical Bonds Video', 'Practice Problems', 'Homework'] ['Chemical Bonds Video', 'Practice Problems', 'Homework']
""" """
seq_css = 'ol#sequence-list>li>a>p' seq_css = 'ol#sequence-list>li>a>p'
return self.css_map(seq_css, self._clean_seq_titles) return self.q(css=seq_css).map(self._clean_seq_titles).results
def go_to_section(self, section_title, subsection_title): def go_to_section(self, section_title, subsection_title):
""" """
...@@ -73,7 +71,7 @@ class CourseNavPage(PageObject): ...@@ -73,7 +71,7 @@ class CourseNavPage(PageObject):
""" """
# For test stability, disable JQuery animations (opening / closing menus) # For test stability, disable JQuery animations (opening / closing menus)
self.disable_jquery_animations() self.browser.execute_script("jQuery.fx.off = true;")
# Get the section by index # Get the section by index
try: try:
...@@ -85,7 +83,7 @@ class CourseNavPage(PageObject): ...@@ -85,7 +83,7 @@ class CourseNavPage(PageObject):
# Click the section to ensure it's open (no harm in clicking twice if it's already open) # Click the section to ensure it's open (no harm in clicking twice if it's already open)
# Add one to convert from list index to CSS index # Add one to convert from list index to CSS index
section_css = 'nav>div.chapter:nth-of-type({0})>h3>a'.format(sec_index + 1) section_css = 'nav>div.chapter:nth-of-type({0})>h3>a'.format(sec_index + 1)
self.css_click(section_css) self.q(css=section_css).first.click()
# Get the subsection by index # Get the subsection by index
try: try:
...@@ -101,8 +99,9 @@ class CourseNavPage(PageObject): ...@@ -101,8 +99,9 @@ class CourseNavPage(PageObject):
) )
# Click the subsection and ensure that the page finishes reloading # Click the subsection and ensure that the page finishes reloading
with fulfill_after(self._on_section_promise(section_title, subsection_title)): self.q(css=subsection_css).first.click()
self.css_click(subsection_css) self._on_section_promise(section_title, subsection_title).fulfill()
def go_to_sequential(self, sequential_title): def go_to_sequential(self, sequential_title):
""" """
...@@ -126,14 +125,14 @@ class CourseNavPage(PageObject): ...@@ -126,14 +125,14 @@ class CourseNavPage(PageObject):
# Click on the sequence item at the correct index # Click on the sequence item at the correct index
# Convert the list index (starts at 0) to a CSS index (starts at 1) # Convert the list index (starts at 0) to a CSS index (starts at 1)
seq_css = "ol#sequence-list>li:nth-of-type({0})>a".format(seq_index + 1) seq_css = "ol#sequence-list>li:nth-of-type({0})>a".format(seq_index + 1)
self.css_click(seq_css) self.q(css=seq_css).first.click()
def _section_titles(self): def _section_titles(self):
""" """
Return a list of all section titles on the page. Return a list of all section titles on the page.
""" """
chapter_css = 'nav>div.chapter>h3>a' chapter_css = 'nav > div.chapter > h3 > a'
return self.css_map(chapter_css, lambda el: el.text.strip()) return self.q(css=chapter_css).map(lambda el: el.text.strip()).results
def _subsection_titles(self, section_index): def _subsection_titles(self, section_index):
""" """
...@@ -148,10 +147,10 @@ class CourseNavPage(PageObject): ...@@ -148,10 +147,10 @@ class CourseNavPage(PageObject):
# Otherwise, we need to get the HTML # Otherwise, we need to get the HTML
# It *would* make sense to always get the HTML, but unfortunately # It *would* make sense to always get the HTML, but unfortunately
# the open tab has some child <span> tags that we don't want. # the open tab has some child <span> tags that we don't want.
return self.css_map( return self.q(
subsection_css, css=subsection_css).map(
lambda el: el.text.strip().split('\n')[0] if el.visible else el.html.strip() lambda el: el.text.strip().split('\n')[0] if el.is_displayed() else el.get_attribute('innerHTML').strip()
) ).results
def _on_section_promise(self, section_title, subsection_title): def _on_section_promise(self, section_title, subsection_title):
""" """
...@@ -172,8 +171,8 @@ class CourseNavPage(PageObject): ...@@ -172,8 +171,8 @@ class CourseNavPage(PageObject):
That's true right after we click the section/subsection, but not true in general That's true right after we click the section/subsection, but not true in general
(the user could go to a section, then expand another tab). (the user could go to a section, then expand another tab).
""" """
current_section_list = self.css_text('nav>div.chapter.is-open>h3>a') current_section_list = self.q(css='nav>div.chapter.is-open>h3>a').text
current_subsection_list = self.css_text('nav>div.chapter.is-open li.active>a>p') current_subsection_list = self.q(css='nav>div.chapter.is-open li.active>a>p').text
if len(current_section_list) == 0: if len(current_section_list) == 0:
self.warning("Could not find the current section") self.warning("Could not find the current section")
...@@ -196,4 +195,4 @@ class CourseNavPage(PageObject): ...@@ -196,4 +195,4 @@ class CourseNavPage(PageObject):
""" """
Clean HTML of sequence titles, stripping out span tags and returning the first line. Clean HTML of sequence titles, stripping out span tags and returning the first line.
""" """
return self.REMOVE_SPAN_TAG_RE.sub('', element.html).strip().split('\n')[0] return self.REMOVE_SPAN_TAG_RE.sub('', element.get_attribute('innerHTML')).strip().split('\n')[0]
...@@ -4,6 +4,7 @@ Student dashboard page. ...@@ -4,6 +4,7 @@ Student dashboard page.
""" """
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise
from . import BASE_URL from . import BASE_URL
...@@ -16,11 +17,11 @@ class DashboardPage(PageObject): ...@@ -16,11 +17,11 @@ class DashboardPage(PageObject):
url = BASE_URL + "/dashboard" url = BASE_URL + "/dashboard"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('section.my-courses') return self.q(css='section.my-courses').present
@property @property
def current_courses_text(self): def current_courses_text(self):
text_items = self.css_text('section#my-courses') text_items = self.q(css='section#my-courses').text
if len(text_items) > 0: if len(text_items) > 0:
return text_items[0] return text_items[0]
else: else:
...@@ -36,7 +37,7 @@ class DashboardPage(PageObject): ...@@ -36,7 +37,7 @@ class DashboardPage(PageObject):
_, course_name = el.text.split(' ', 1) _, course_name = el.text.split(' ', 1)
return course_name return course_name
return self.css_map('section.info > hgroup > h3 > a', _get_course_name) return self.q(css='section.info > hgroup > h3 > a').map(_get_course_name).results
def view_course(self, course_id): def view_course(self, course_id):
""" """
...@@ -45,7 +46,7 @@ class DashboardPage(PageObject): ...@@ -45,7 +46,7 @@ class DashboardPage(PageObject):
link_css = self._link_css(course_id) link_css = self._link_css(course_id)
if link_css is not None: if link_css is not None:
self.css_click(link_css) self.q(css=link_css).first.click()
else: else:
msg = "No links found for course {0}".format(course_id) msg = "No links found for course {0}".format(course_id)
self.warning(msg) self.warning(msg)
...@@ -55,7 +56,7 @@ class DashboardPage(PageObject): ...@@ -55,7 +56,7 @@ class DashboardPage(PageObject):
Return a CSS selector for the link to the course with `course_id`. Return a CSS selector for the link to the course with `course_id`.
""" """
# Get the link hrefs for all courses # Get the link hrefs for all courses
all_links = self.css_map('a.enter-course', lambda el: el['href']) all_links = self.q(css='a.enter-course').map(lambda el: el.get_attribute('href')).results
# Search for the first link that matches the course id # Search for the first link that matches the course id
link_index = None link_index = None
...@@ -73,6 +74,13 @@ class DashboardPage(PageObject): ...@@ -73,6 +74,13 @@ class DashboardPage(PageObject):
""" """
Change the language on the dashboard to the language corresponding with `code`. Change the language on the dashboard to the language corresponding with `code`.
""" """
self.css_click(".edit-language") self.q(css=".edit-language").first.click()
self.select_option("language", code) self.q(css='select[name="language"] option[value="{}"]'.format(code)).first.click()
self.css_click("#submit-lang") self.q(css="#submit-lang").first.click()
self._changed_lang_promise(code).fulfill()
def _changed_lang_promise(self, code):
def _check_func():
return self.q(css='select[name="language"] option[value="{}"]'.format(code)).selected
return EmptyPromise(_check_func, "language changed")
from bok_choy.page_object import unguarded from bok_choy.page_object import unguarded
from bok_choy.promise import EmptyPromise, fulfill from bok_choy.promise import EmptyPromise
from .course_page import CoursePage from .course_page import CoursePage
...@@ -10,9 +10,9 @@ class DiscussionSingleThreadPage(CoursePage): ...@@ -10,9 +10,9 @@ class DiscussionSingleThreadPage(CoursePage):
self.thread_id = thread_id self.thread_id = thread_id
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present( return self.q(
"body.discussion .discussion-article[data-id='{thread_id}']".format(thread_id=self.thread_id) css="body.discussion .discussion-article[data-id='{thread_id}']".format(thread_id=self.thread_id)
) ).present
@property @property
@unguarded @unguarded
...@@ -24,7 +24,7 @@ class DiscussionSingleThreadPage(CoursePage): ...@@ -24,7 +24,7 @@ class DiscussionSingleThreadPage(CoursePage):
Returns the text of the first element matching the given selector, or Returns the text of the first element matching the given selector, or
None if no such element exists None if no such element exists
""" """
text_list = self.css_text(selector) text_list = self.q(css=selector).text
return text_list[0] if text_list else None return text_list[0] if text_list else None
def get_response_total_text(self): def get_response_total_text(self):
...@@ -33,7 +33,7 @@ class DiscussionSingleThreadPage(CoursePage): ...@@ -33,7 +33,7 @@ class DiscussionSingleThreadPage(CoursePage):
def get_num_displayed_responses(self): def get_num_displayed_responses(self):
"""Returns the number of responses actually rendered""" """Returns the number of responses actually rendered"""
return self.css_count(".discussion-response") return len(self.q(css=".discussion-response").results)
def get_shown_responses_text(self): def get_shown_responses_text(self):
"""Returns the shown response count text, or None if not present""" """Returns the shown response count text, or None if not present"""
...@@ -44,12 +44,16 @@ class DiscussionSingleThreadPage(CoursePage): ...@@ -44,12 +44,16 @@ class DiscussionSingleThreadPage(CoursePage):
return self._get_element_text(".load-response-button") return self._get_element_text(".load-response-button")
def load_more_responses(self): def load_more_responses(self):
"""Clicks the laod more responses button and waits for responses to load""" """Clicks the load more responses button and waits for responses to load"""
self.css_click(".load-response-button") self.q(css=".load-response-button").first.click()
fulfill(EmptyPromise(
lambda: not self.is_css_present(".loading"), def _is_ajax_finished():
"Loading more responses completed" return self.browser.execute_script("return jQuery.active") == 0
))
EmptyPromise(
_is_ajax_finished,
"Loading more Responses"
).fulfill()
def has_add_response_button(self): def has_add_response_button(self):
"""Returns true if the add response button is visible, false otherwise""" """Returns true if the add response button is visible, false otherwise"""
...@@ -60,14 +64,17 @@ class DiscussionSingleThreadPage(CoursePage): ...@@ -60,14 +64,17 @@ class DiscussionSingleThreadPage(CoursePage):
Clicks the add response button and ensures that the response text Clicks the add response button and ensures that the response text
field receives focus field receives focus
""" """
self.css_click(".add-response-btn") self.q(css=".add-response-btn").first.click()
fulfill(EmptyPromise( EmptyPromise(
lambda: self.is_css_present("#wmd-input-reply-body-{thread_id}:focus".format(thread_id=self.thread_id)), lambda: self.q(css="#wmd-input-reply-body-{thread_id}:focus".format(thread_id=self.thread_id)),
"Response field received focus" "Response field received focus"
)) ).fulfill()
def _is_element_visible(self, selector): def _is_element_visible(self, selector):
return any(self.css_map(selector, lambda el: el.visible)) return (
self.q(css=selector).present and
self.q(css=selector).visible
)
def is_response_editor_visible(self, response_id): def is_response_editor_visible(self, response_id):
"""Returns true if the response editor is present, false otherwise""" """Returns true if the response editor is present, false otherwise"""
...@@ -75,15 +82,15 @@ class DiscussionSingleThreadPage(CoursePage): ...@@ -75,15 +82,15 @@ class DiscussionSingleThreadPage(CoursePage):
def start_response_edit(self, response_id): def start_response_edit(self, response_id):
"""Click the edit button for the response, loading the editing view""" """Click the edit button for the response, loading the editing view"""
self.css_click(".response_{} .discussion-response .action-edit".format(response_id)) self.q(css=".response_{} .discussion-response .action-edit".format(response_id)).first.click()
fulfill(EmptyPromise( EmptyPromise(
lambda: self.is_response_editor_visible(response_id), lambda: self.is_response_editor_visible(response_id),
"Response edit started" "Response edit started"
)) ).fulfill()
def is_add_comment_visible(self, response_id): def is_add_comment_visible(self, response_id):
"""Returns true if the "add comment" form is visible for a response""" """Returns true if the "add comment" form is visible for a response"""
return self._is_element_visible(".response_{} .new-comment".format(response_id)) return self._is_element_visible("#wmd-input-comment-body-{}".format(response_id))
def is_comment_visible(self, comment_id): def is_comment_visible(self, comment_id):
"""Returns true if the comment is viewable onscreen""" """Returns true if the comment is viewable onscreen"""
...@@ -98,11 +105,11 @@ class DiscussionSingleThreadPage(CoursePage): ...@@ -98,11 +105,11 @@ class DiscussionSingleThreadPage(CoursePage):
def delete_comment(self, comment_id): def delete_comment(self, comment_id):
with self.handle_alert(): with self.handle_alert():
self.css_click("#comment_{} div.action-delete".format(comment_id)) self.q(css="#comment_{} div.action-delete".format(comment_id)).first.click()
fulfill(EmptyPromise( EmptyPromise(
lambda: not self.is_comment_visible(comment_id), lambda: not self.is_comment_visible(comment_id),
"Deleted comment was removed" "Deleted comment was removed"
)) ).fulfill()
def is_comment_editable(self, comment_id): def is_comment_editable(self, comment_id):
"""Returns true if the edit comment button is present, false otherwise""" """Returns true if the edit comment button is present, false otherwise"""
...@@ -110,49 +117,48 @@ class DiscussionSingleThreadPage(CoursePage): ...@@ -110,49 +117,48 @@ class DiscussionSingleThreadPage(CoursePage):
def is_comment_editor_visible(self, comment_id): def is_comment_editor_visible(self, comment_id):
"""Returns true if the comment editor is present, false otherwise""" """Returns true if the comment editor is present, false otherwise"""
return self._is_element_visible("#comment_{} .edit-comment-body".format(comment_id)) return self._is_element_visible(".edit-comment-body[data-id='{}']".format(comment_id))
def _get_comment_editor_value(self, comment_id): def _get_comment_editor_value(self, comment_id):
return self.css_value("#comment_{} .wmd-input".format(comment_id))[0] return self.q(css="#wmd-input-edit-comment-body-{}".format(comment_id)).text[0]
def start_comment_edit(self, comment_id): def start_comment_edit(self, comment_id):
"""Click the edit button for the comment, loading the editing view""" """Click the edit button for the comment, loading the editing view"""
old_body = self.get_comment_body(comment_id) old_body = self.get_comment_body(comment_id)
self.css_click("#comment_{} .action-edit".format(comment_id)) self.q(css="#comment_{} .action-edit".format(comment_id)).first.click()
fulfill(EmptyPromise( EmptyPromise(
lambda: ( lambda: (
self.is_comment_editor_visible(comment_id) and self.is_comment_editor_visible(comment_id) and
not self.is_comment_visible(comment_id) and not self.is_comment_visible(comment_id) and
self._get_comment_editor_value(comment_id) == old_body self._get_comment_editor_value(comment_id) == old_body
), ),
"Comment edit started" "Comment edit started"
)) ).fulfill()
def set_comment_editor_value(self, comment_id, new_body): def set_comment_editor_value(self, comment_id, new_body):
"""Replace the contents of the comment editor""" """Replace the contents of the comment editor"""
self.css_fill("#comment_{} .wmd-input".format(comment_id), new_body) self.q(css="#comment_{} .wmd-input".format(comment_id)).fill(new_body)
def submit_comment_edit(self, comment_id): def submit_comment_edit(self, comment_id, new_comment_body):
"""Click the submit button on the comment editor""" """Click the submit button on the comment editor"""
new_body = self._get_comment_editor_value(comment_id) self.q(css="#comment_{} .post-update".format(comment_id)).first.click()
self.css_click("#comment_{} .post-update".format(comment_id)) EmptyPromise(
fulfill(EmptyPromise(
lambda: ( lambda: (
not self.is_comment_editor_visible(comment_id) and not self.is_comment_editor_visible(comment_id) and
self.is_comment_visible(comment_id) and self.is_comment_visible(comment_id) and
self.get_comment_body(comment_id) == new_body self.get_comment_body(comment_id) == new_comment_body
), ),
"Comment edit succeeded" "Comment edit succeeded"
)) ).fulfill()
def cancel_comment_edit(self, comment_id, original_body): def cancel_comment_edit(self, comment_id, original_body):
"""Click the cancel button on the comment editor""" """Click the cancel button on the comment editor"""
self.css_click("#comment_{} .post-cancel".format(comment_id)) self.q(css="#comment_{} .post-cancel".format(comment_id)).first.click()
fulfill(EmptyPromise( EmptyPromise(
lambda: ( lambda: (
not self.is_comment_editor_visible(comment_id) and not self.is_comment_editor_visible(comment_id) and
self.is_comment_visible(comment_id) and self.is_comment_visible(comment_id) and
self.get_comment_body(comment_id) == original_body self.get_comment_body(comment_id) == original_body
), ),
"Comment edit was canceled" "Comment edit was canceled"
)) ).fulfill()
...@@ -23,4 +23,5 @@ class FindCoursesPage(PageObject): ...@@ -23,4 +23,5 @@ class FindCoursesPage(PageObject):
Retrieve the list of available course IDs Retrieve the list of available course IDs
on the page. on the page.
""" """
return self.css_map('article.course', lambda el: el['id'])
return self.q(css='article.course').attrs('id')
...@@ -3,7 +3,7 @@ Login page for the LMS. ...@@ -3,7 +3,7 @@ Login page for the LMS.
""" """
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise, fulfill_after from bok_choy.promise import EmptyPromise
from . import BASE_URL from . import BASE_URL
...@@ -17,13 +17,27 @@ class LoginPage(PageObject): ...@@ -17,13 +17,27 @@ class LoginPage(PageObject):
def is_browser_on_page(self): def is_browser_on_page(self):
return any([ return any([
'log in' in title.lower() 'log in' in title.lower()
for title in self.css_text('span.title-super') for title in self.q(css='span.title-super').text
]) ])
def login(self, email, password): def login(self, email, password):
""" """
Attempt to log in using `email` and `password`. Attempt to log in using `email` and `password`.
""" """
EmptyPromise(self.q(css='input#email').is_present, "Click ready").fulfill()
EmptyPromise(self.q(css='input#password').is_present, "Click ready").fulfill()
self.q(css='input#email').fill(email)
self.q(css='input#password').fill(password)
self.q(css='button#submit').click()
EmptyPromise(
lambda: "login" not in self.browser.url,
"redirected from the login page"
)
"""
# Ensure that we make it to another page # Ensure that we make it to another page
on_next_page = EmptyPromise( on_next_page = EmptyPromise(
lambda: "login" not in self.browser.url, lambda: "login" not in self.browser.url,
...@@ -34,3 +48,5 @@ class LoginPage(PageObject): ...@@ -34,3 +48,5 @@ class LoginPage(PageObject):
self.css_fill('input#email', email) self.css_fill('input#email', email)
self.css_fill('input#password', password) self.css_fill('input#password', password)
self.css_click('button#submit') self.css_click('button#submit')
"""
...@@ -3,7 +3,7 @@ Open-ended response in the courseware. ...@@ -3,7 +3,7 @@ Open-ended response in the courseware.
""" """
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise, fulfill_after, fulfill from bok_choy.promise import EmptyPromise
from .rubric import RubricPage from .rubric import RubricPage
...@@ -15,7 +15,7 @@ class OpenResponsePage(PageObject): ...@@ -15,7 +15,7 @@ class OpenResponsePage(PageObject):
url = None url = None
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('div.xmodule_CombinedOpenEndedModule') return self.q(css='div.xmodule_CombinedOpenEndedModule').present
@property @property
def assessment_type(self): def assessment_type(self):
...@@ -23,7 +23,7 @@ class OpenResponsePage(PageObject): ...@@ -23,7 +23,7 @@ class OpenResponsePage(PageObject):
Return the type of assessment currently active. Return the type of assessment currently active.
Options are "self", "ai", or "peer" Options are "self", "ai", or "peer"
""" """
labels = self.css_text('section#combined-open-ended-status>div.statusitem-current') labels = self.q(css='section#combined-open-ended-status>div.statusitem-current').text
if len(labels) < 1: if len(labels) < 1:
self.warning("Could not find assessment type label") self.warning("Could not find assessment type label")
...@@ -46,7 +46,7 @@ class OpenResponsePage(PageObject): ...@@ -46,7 +46,7 @@ class OpenResponsePage(PageObject):
Return an HTML string representing the essay prompt. Return an HTML string representing the essay prompt.
""" """
prompt_css = "section.open-ended-child>div.prompt" prompt_css = "section.open-ended-child>div.prompt"
prompts = self.css_map(prompt_css, lambda el: el.html.strip()) prompts = self.q(css=prompt_css).map(lambda el: el.get_attribute('innerHTML').strip()).results
if len(prompts) == 0: if len(prompts) == 0:
self.warning("Could not find essay prompt on page.") self.warning("Could not find essay prompt on page.")
...@@ -73,7 +73,7 @@ class OpenResponsePage(PageObject): ...@@ -73,7 +73,7 @@ class OpenResponsePage(PageObject):
Return the written feedback from the grader (if any). Return the written feedback from the grader (if any).
If no feedback available, returns None. If no feedback available, returns None.
""" """
feedback = self.css_text('div.written-feedback') feedback = self.q(css='div.written-feedback').text
if len(feedback) > 0: if len(feedback) > 0:
return feedback[0] return feedback[0]
...@@ -85,7 +85,7 @@ class OpenResponsePage(PageObject): ...@@ -85,7 +85,7 @@ class OpenResponsePage(PageObject):
""" """
Alert message displayed to the user. Alert message displayed to the user.
""" """
alerts = self.css_text("div.open-ended-alert") alerts = self.q(css="div.open-ended-alert").text
if len(alerts) < 1: if len(alerts) < 1:
return "" return ""
...@@ -98,7 +98,7 @@ class OpenResponsePage(PageObject): ...@@ -98,7 +98,7 @@ class OpenResponsePage(PageObject):
Status message from the grader. Status message from the grader.
If not present, return an empty string. If not present, return an empty string.
""" """
status_list = self.css_text('div.grader-status') status_list = self.q(css='div.grader-status').text
if len(status_list) < 1: if len(status_list) < 1:
self.warning("No grader status found") self.warning("No grader status found")
...@@ -114,27 +114,26 @@ class OpenResponsePage(PageObject): ...@@ -114,27 +114,26 @@ class OpenResponsePage(PageObject):
Input a response to the prompt. Input a response to the prompt.
""" """
input_css = "textarea.short-form-response" input_css = "textarea.short-form-response"
self.css_fill(input_css, response_str) self.q(css=input_css).fill(response_str)
def save_response(self): def save_response(self):
""" """
Save the response for later submission. Save the response for later submission.
""" """
status_msg_shown = EmptyPromise( self.q(css='input.save-button').first.click()
EmptyPromise(
lambda: 'save' in self.alert_message.lower(), lambda: 'save' in self.alert_message.lower(),
"Status message saved" "Status message saved"
) ).fulfill()
with fulfill_after(status_msg_shown):
self.css_click('input.save-button')
def submit_response(self): def submit_response(self):
""" """
Submit a response for grading. Submit a response for grading.
""" """
self.css_click('input.submit-button') self.q(css='input.submit-button').first.click()
# modal dialog confirmation # modal dialog confirmation
self.css_click('button.ok-button') self.q(css='button.ok-button').first.click()
# Ensure that the submission completes # Ensure that the submission completes
self._wait_for_submitted(self.assessment_type) self._wait_for_submitted(self.assessment_type)
...@@ -148,11 +147,11 @@ class OpenResponsePage(PageObject): ...@@ -148,11 +147,11 @@ class OpenResponsePage(PageObject):
RubricPage(self.browser).wait_for_page() RubricPage(self.browser).wait_for_page()
elif assessment_type == 'ai' or assessment_type == "peer": elif assessment_type == 'ai' or assessment_type == "peer":
fulfill(EmptyPromise( EmptyPromise(
lambda: self.grader_status != 'Unanswered', lambda: self.grader_status != 'Unanswered',
"Problem status is no longer 'unanswered'" "Problem status is no longer 'unanswered'"
)) ).fulfill()
else: else:
self.warning("Unrecognized assessment type '{0}'".format(assessment_type)) self.warning("Unrecognized assessment type '{0}'".format(assessment_type))
fulfill(EmptyPromise(lambda: True, "Unrecognized assessment type")) EmptyPromise(lambda: True, "Unrecognized assessment type").fulfill()
...@@ -4,6 +4,7 @@ Page that allows the student to grade calibration essays ...@@ -4,6 +4,7 @@ Page that allows the student to grade calibration essays
""" """
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from bok_choy.promise import Promise
from .rubric import RubricPage from .rubric import RubricPage
...@@ -15,16 +16,21 @@ class PeerCalibratePage(PageObject): ...@@ -15,16 +16,21 @@ class PeerCalibratePage(PageObject):
url = None url = None
def is_browser_on_page(self): def is_browser_on_page(self):
return (
self.is_css_present('div.peer-grading-tools') or def _is_correct_page():
self.is_css_present('div.calibration-panel.current-state') is_present = (
) self.q(css='div.peer-grading-tools').present or
self.q(css='div.calibration-panel.current-state').present
)
return is_present, is_present
return Promise(_is_correct_page, 'On the peer grading calibration page.').fulfill()
def continue_to_grading(self): def continue_to_grading(self):
""" """
Continue to peer grading after completing calibration. Continue to peer grading after completing calibration.
""" """
self.css_click('input.calibration-feedback-button') self.q(css='input.calibration-feedback-button').first.click()
@property @property
def rubric(self): def rubric(self):
...@@ -33,7 +39,7 @@ class PeerCalibratePage(PageObject): ...@@ -33,7 +39,7 @@ class PeerCalibratePage(PageObject):
If no rubric is available, raises a `BrokenPromise` exception. If no rubric is available, raises a `BrokenPromise` exception.
""" """
rubric = RubricPage(self.browser) rubric = RubricPage(self.browser)
rubric.wait_for_page() rubric.wait_for_page(timeout=60)
return rubric return rubric
@property @property
...@@ -41,7 +47,7 @@ class PeerCalibratePage(PageObject): ...@@ -41,7 +47,7 @@ class PeerCalibratePage(PageObject):
""" """
Return a message shown to the user, or None if no message is available. Return a message shown to the user, or None if no message is available.
""" """
messages = self.css_text('div.peer-grading-tools > div.message-container > p') messages = self.q(css='div.peer-grading-tools > div.message-container > p').text
if len(messages) < 1: if len(messages) < 1:
return None return None
else: else:
......
...@@ -3,6 +3,7 @@ Confirmation screen for peer calibration and grading. ...@@ -3,6 +3,7 @@ Confirmation screen for peer calibration and grading.
""" """
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from bok_choy.promise import Promise
class PeerConfirmPage(PageObject): class PeerConfirmPage(PageObject):
...@@ -13,7 +14,12 @@ class PeerConfirmPage(PageObject): ...@@ -13,7 +14,12 @@ class PeerConfirmPage(PageObject):
url = None url = None
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('section.calibration-interstitial-page')
def _is_correct_page():
is_present = self.q(css='section.calibration-interstitial-page').present
return is_present, is_present
return Promise(_is_correct_page, 'On the confirmation page for peer calibration and grading.').fulfill()
def start(self, is_calibrating=False): def start(self, is_calibrating=False):
""" """
...@@ -21,7 +27,6 @@ class PeerConfirmPage(PageObject): ...@@ -21,7 +27,6 @@ class PeerConfirmPage(PageObject):
If `is_calibrating` is false, try to continue to peer grading. If `is_calibrating` is false, try to continue to peer grading.
Otherwise, try to continue to calibration grading. Otherwise, try to continue to calibration grading.
""" """
self.css_click( self.q(css='input.calibration-interstitial-page-button'
'input.calibration-interstitial-page-button'
if is_calibrating else 'input.interstitial-page-button' if is_calibrating else 'input.interstitial-page-button'
) ).first.click()
...@@ -3,6 +3,7 @@ Students grade peer submissions. ...@@ -3,6 +3,7 @@ Students grade peer submissions.
""" """
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from bok_choy.promise import Promise
from .rubric import RubricPage from .rubric import RubricPage
...@@ -14,24 +15,28 @@ class PeerGradePage(PageObject): ...@@ -14,24 +15,28 @@ class PeerGradePage(PageObject):
url = None url = None
def is_browser_on_page(self): def is_browser_on_page(self):
return ( def _is_correct_page():
self.is_css_present('div.peer-grading-tools') or is_present = (
self.is_css_present('div.grading-panel.current-state') self.q(css='div.peer-grading-tools').present or
) self.q(css='div.grading-panel.current-state').present
)
return is_present, is_present
return Promise(_is_correct_page, 'On the peer grading page.').fulfill()
@property @property
def problem_list(self): def problem_list(self):
""" """
Return the list of available problems to peer grade. Return the list of available problems to peer grade.
""" """
return self.css_text('a.problem-button') return self.q(css='a.problem-button').text
def select_problem(self, problem_name): def select_problem(self, problem_name):
""" """
Choose the problem with `problem_name` to start grading or calibrating. Choose the problem with `problem_name` to start grading or calibrating.
""" """
index = self.problem_list.index(problem_name) + 1 index = self.problem_list.index(problem_name) + 1
self.css_click('a.problem-button:nth-of-type({})'.format(index)) self.q(css='a.problem-button:nth-of-type({})'.format(index)).first.click()
@property @property
def rubric(self): def rubric(self):
......
...@@ -12,9 +12,10 @@ class ProgressPage(CoursePage): ...@@ -12,9 +12,10 @@ class ProgressPage(CoursePage):
url_path = "progress" url_path = "progress"
#@property
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.q(css='div.course-info').present
has_graph = self.is_css_present('div#grade-detail-graph') has_graph = self.q(css='div#grade-detail-graph').present
return has_course_info and has_graph return has_course_info and has_graph
def scores(self, chapter, section): def scores(self, chapter, section):
...@@ -46,7 +47,7 @@ class ProgressPage(CoursePage): ...@@ -46,7 +47,7 @@ class ProgressPage(CoursePage):
Returns `None` if it cannot find such a chapter. Returns `None` if it cannot find such a chapter.
""" """
chapter_css = 'div.chapters section h2' chapter_css = 'div.chapters section h2'
chapter_titles = self.css_map(chapter_css, lambda el: el.text.lower().strip()) chapter_titles = self.q(css=chapter_css).map(lambda el: el.text.lower().strip()).results
try: try:
# CSS indices are 1-indexed, so add one to the list index # CSS indices are 1-indexed, so add one to the list index
...@@ -65,7 +66,7 @@ class ProgressPage(CoursePage): ...@@ -65,7 +66,7 @@ class ProgressPage(CoursePage):
# Get the links containing the section titles in `chapter_index`. # Get the links containing the section titles in `chapter_index`.
# The link text is the section title. # The link text is the section title.
section_css = 'div.chapters>section:nth-of-type({0}) div.sections div h3 a'.format(chapter_index) section_css = 'div.chapters>section:nth-of-type({0}) div.sections div h3 a'.format(chapter_index)
section_titles = self.css_map(section_css, lambda el: el.text.lower().strip()) section_titles = self.q(css=section_css).map(lambda el: el.text.lower().strip()).results
# The section titles also contain "n of m possible points" on the second line # The section titles also contain "n of m possible points" on the second line
# We have to remove this to find the right title # We have to remove this to find the right title
...@@ -95,7 +96,7 @@ class ProgressPage(CoursePage): ...@@ -95,7 +96,7 @@ class ProgressPage(CoursePage):
chapter_index, section_index chapter_index, section_index
) )
text_scores = self.css_text(score_css) text_scores = self.q(css=score_css).text
# Convert text scores to tuples of (points, max_points) # Convert text scores to tuples of (points, max_points)
return [tuple(map(int, score.split('/'))) for score in text_scores] return [tuple(map(int, score.split('/'))) for score in text_scores]
...@@ -34,7 +34,7 @@ class RegisterPage(PageObject): ...@@ -34,7 +34,7 @@ class RegisterPage(PageObject):
def is_browser_on_page(self): def is_browser_on_page(self):
return any([ return any([
'register' in title.lower() 'register' in title.lower()
for title in self.css_text('span.title-sub') for title in self.q(css='span.title-sub').text
]) ])
def provide_info(self, email, password, username, full_name): def provide_info(self, email, password, username, full_name):
...@@ -42,18 +42,18 @@ class RegisterPage(PageObject): ...@@ -42,18 +42,18 @@ class RegisterPage(PageObject):
Fill in registration info. Fill in registration info.
`email`, `password`, `username`, and `full_name` are the user's credentials. `email`, `password`, `username`, and `full_name` are the user's credentials.
""" """
self.css_fill('input#email', email) self.q(css='input#email').fill(email)
self.css_fill('input#password', password) self.q(css='input#password').fill(password)
self.css_fill('input#username', username) self.q(css='input#username').fill(username)
self.css_fill('input#name', full_name) self.q(css='input#name').fill(full_name)
self.css_check('input#tos-yes') self.q(css='input#tos-yes').first.click()
self.css_check('input#honorcode-yes') self.q(css='input#honorcode-yes').first.click()
def submit(self): def submit(self):
""" """
Submit registration info to create an account. Submit registration info to create an account.
""" """
self.css_click('button#submit') self.q(css='button#submit').first.click()
# The next page is the dashboard; make sure it loads # The next page is the dashboard; make sure it loads
dashboard = DashboardPage(self.browser) dashboard = DashboardPage(self.browser)
......
...@@ -3,7 +3,7 @@ Rubric for open-ended response problems, including calibration and peer-grading. ...@@ -3,7 +3,7 @@ Rubric for open-ended response problems, including calibration and peer-grading.
""" """
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise, fulfill_after, fulfill_before from bok_choy.promise import EmptyPromise
class ScoreMismatchError(Exception): class ScoreMismatchError(Exception):
...@@ -24,7 +24,7 @@ class RubricPage(PageObject): ...@@ -24,7 +24,7 @@ class RubricPage(PageObject):
""" """
Return a boolean indicating whether the rubric is available. Return a boolean indicating whether the rubric is available.
""" """
return self.is_css_present('div.rubric') return self.q(css='div.rubric').present
@property @property
def categories(self): def categories(self):
...@@ -37,7 +37,7 @@ class RubricPage(PageObject): ...@@ -37,7 +37,7 @@ class RubricPage(PageObject):
The rubric is not always visible; if it's not available, The rubric is not always visible; if it's not available,
this will return an empty list. this will return an empty list.
""" """
return self.css_text('span.rubric-category') return self.q(css='span.rubric-category').text
def set_scores(self, scores): def set_scores(self, scores):
""" """
...@@ -60,10 +60,9 @@ class RubricPage(PageObject): ...@@ -60,10 +60,9 @@ class RubricPage(PageObject):
# Set the score for each category # Set the score for each category
for score_index in range(len(scores)): for score_index in range(len(scores)):
# Check that we have the enough radio buttons # Check that we have the enough radio buttons
category_css = "div.rubric>ul.rubric-list:nth-of-type({0})".format(score_index + 1) category_css = "div.rubric>ul.rubric-list:nth-of-type({0})".format(score_index + 1)
if scores[score_index] > self.css_count(category_css + ' input.score-selection'): if scores[score_index] > len(self.q(css=category_css + ' input.score-selection').results):
raise ScoreMismatchError( raise ScoreMismatchError(
"Tried to select score {0} but there are only {1} options".format( "Tried to select score {0} but there are only {1} options".format(
score_index, len(scores))) score_index, len(scores)))
...@@ -74,7 +73,12 @@ class RubricPage(PageObject): ...@@ -74,7 +73,12 @@ class RubricPage(PageObject):
category_css + category_css +
">li.rubric-list-item:nth-of-type({0}) input.score-selection".format(scores[score_index] + 1) ">li.rubric-list-item:nth-of-type({0}) input.score-selection".format(scores[score_index] + 1)
) )
self.css_check(input_css)
EmptyPromise(lambda: self._select_score_radio_button(input_css), "Score selection failed.").fulfill()
def _select_score_radio_button(self, radio_button_css):
self.q(css=radio_button_css).first.click()
return self.q(css=radio_button_css).selected
@property @property
def feedback(self): def feedback(self):
...@@ -86,14 +90,13 @@ class RubricPage(PageObject): ...@@ -86,14 +90,13 @@ class RubricPage(PageObject):
If feedback could not be interpreted (unexpected CSS class), If feedback could not be interpreted (unexpected CSS class),
the list will contain a `None` item. the list will contain a `None` item.
""" """
# Get the green checkmark / red x labels # Get the green checkmark / red x labels
# We need to filter out the similar-looking CSS classes # We need to filter out the similar-looking CSS classes
# for the rubric items that are NOT marked correct/incorrect # for the rubric items that are NOT marked correct/incorrect
feedback_css = 'div.rubric-label>label' feedback_css = 'div.rubric-label>label'
labels = [ labels = [
el_class for el_class in el_class for el_class in
self.css_map(feedback_css, lambda el: el['class']) self.q(css=feedback_css).attrs('class')
if el_class != 'rubric-elements-info' if el_class != 'rubric-elements-info'
] ]
...@@ -110,17 +113,29 @@ class RubricPage(PageObject): ...@@ -110,17 +113,29 @@ class RubricPage(PageObject):
return map(map_feedback, labels) return map(map_feedback, labels)
def submit(self): def submit(self, promise_check_type=None):
""" """
Submit the rubric. Submit the rubric.
`promise_check_type` is either 'self', or 'peer'. If promise check is not required then don't pass any value.
""" """
# Wait for the button to become enabled # Wait for the button to become enabled
button_css = 'input.submit-button' button_css = 'input.submit-button'
button_enabled = EmptyPromise(
lambda: all(self.css_map(button_css, lambda el: not el['disabled'])), EmptyPromise(
"Submit button enabled" lambda: all(self.q(css=button_css).map(lambda el: not el.get_attribute('disabled')).results),
) "Submit button not enabled"
).fulfill()
# Submit the assessment # Submit the assessment
with fulfill_before(button_enabled): self.q(css=button_css).first.click()
self.css_click(button_css)
if promise_check_type == 'self':
# Check if submitted rubric is available
EmptyPromise(
lambda: self.q(css='div.rubric-label>label').present, 'Submitted Rubric not available!'
).fulfill()
elif promise_check_type == 'peer':
# Check if we are ready for peer grading
EmptyPromise(
lambda: self.q(css='input.calibration-feedback-button').present, 'Not ready for peer grading!'
).fulfill()
...@@ -3,7 +3,7 @@ High-level tab navigation. ...@@ -3,7 +3,7 @@ High-level tab navigation.
""" """
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from bok_choy.promise import Promise, EmptyPromise, fulfill_after, fulfill from bok_choy.promise import Promise, EmptyPromise
class TabNavPage(PageObject): class TabNavPage(PageObject):
...@@ -14,12 +14,13 @@ class TabNavPage(PageObject): ...@@ -14,12 +14,13 @@ class TabNavPage(PageObject):
url = None url = None
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('ol.course-tabs') return self.q(css='ol.course-tabs').present
def go_to_tab(self, tab_name): def go_to_tab(self, tab_name):
""" """
Navigate to the tab `tab_name`. Navigate to the tab `tab_name`.
""" """
if tab_name not in ['Courseware', 'Course Info', 'Discussion', 'Wiki', 'Progress']: if tab_name not in ['Courseware', 'Course Info', 'Discussion', 'Wiki', 'Progress']:
self.warning("'{0}' is not a valid tab name".format(tab_name)) self.warning("'{0}' is not a valid tab name".format(tab_name))
...@@ -27,11 +28,12 @@ class TabNavPage(PageObject): ...@@ -27,11 +28,12 @@ class TabNavPage(PageObject):
# so we find the tab with `tab_name` in its text. # so we find the tab with `tab_name` in its text.
tab_css = self._tab_css(tab_name) tab_css = self._tab_css(tab_name)
with fulfill_after(self._is_on_tab_promise(tab_name)): if tab_css is not None:
if tab_css is not None: self.q(css=tab_css).first.click()
self.css_click(tab_css) else:
else: self.warning("No tabs found for '{0}'".format(tab_name))
self.warning("No tabs found for '{0}'".format(tab_name))
self._is_on_tab_promise(tab_name).fulfill()
def is_on_tab(self, tab_name): def is_on_tab(self, tab_name):
""" """
...@@ -63,10 +65,10 @@ class TabNavPage(PageObject): ...@@ -63,10 +65,10 @@ class TabNavPage(PageObject):
if the tab names fail to load. if the tab names fail to load.
""" """
def _check_func(): def _check_func():
tab_names = self.css_text('ol.course-tabs li a') tab_names = self.q(css='ol.course-tabs li a').text
return (len(tab_names) > 0, tab_names) return (len(tab_names) > 0, tab_names)
return fulfill(Promise(_check_func, "Get all tab names")) return Promise(_check_func, "Get all tab names").fulfill()
def _is_on_tab(self, tab_name): def _is_on_tab(self, tab_name):
""" """
...@@ -74,14 +76,13 @@ class TabNavPage(PageObject): ...@@ -74,14 +76,13 @@ class TabNavPage(PageObject):
This is a private method, so it does NOT enforce the page check, This is a private method, so it does NOT enforce the page check,
which is what we want when we're polling the DOM in a promise. which is what we want when we're polling the DOM in a promise.
""" """
current_tab_list = self.css_text('ol.course-tabs>li>a.active') current_tab_list = self.q(css='ol.course-tabs > li > a.active').text
if len(current_tab_list) == 0: if len(current_tab_list) == 0:
self.warning("Could not find current tab") self.warning("Could not find current tab")
return False return False
else: else:
return (current_tab_list[0].strip().split('\n')[0] == tab_name) return current_tab_list[0].strip().split('\n')[0] == tab_name
def _is_on_tab_promise(self, tab_name): def _is_on_tab_promise(self, tab_name):
......
...@@ -4,7 +4,7 @@ Video player in the courseware. ...@@ -4,7 +4,7 @@ Video player in the courseware.
import time import time
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise, fulfill_after from bok_choy.promise import EmptyPromise
from bok_choy.javascript import wait_for_js, js_defined from bok_choy.javascript import wait_for_js, js_defined
...@@ -17,7 +17,7 @@ class VideoPage(PageObject): ...@@ -17,7 +17,7 @@ class VideoPage(PageObject):
url = None url = None
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('div.xmodule_VideoModule') return self.q(css='div.xmodule_VideoModule').present
@property @property
def elapsed_time(self): def elapsed_time(self):
...@@ -40,37 +40,37 @@ class VideoPage(PageObject): ...@@ -40,37 +40,37 @@ class VideoPage(PageObject):
""" """
Return a boolean indicating whether the video is playing. Return a boolean indicating whether the video is playing.
""" """
return self.is_css_present('a.video_control') and self.is_css_present('a.video_control.pause') return self.q(css='a.video_control').present and self.q(css='a.video_control.pause').present
@property @property
def is_paused(self): def is_paused(self):
""" """
Return a boolean indicating whether the video is paused. Return a boolean indicating whether the video is paused.
""" """
return self.is_css_present('a.video_control') and self.is_css_present('a.video_control.play') return self.q(css='a.video_control').present and self.q(css='a.video_control.play').present
@wait_for_js @wait_for_js
def play(self): def play(self):
""" """
Start playing the video. Start playing the video.
""" """
with fulfill_after(EmptyPromise(lambda: self.is_playing, "Video is playing")): self.q(css='a.video_control.play').first.click()
self.css_click('a.video_control.play') EmptyPromise(lambda: self.is_playing, "Video is playing")
@wait_for_js @wait_for_js
def pause(self): def pause(self):
""" """
Pause the video. Pause the video.
""" """
with fulfill_after(EmptyPromise(lambda: self.is_paused, "Video is paused")): self.q(css='a.video_control.pause').first.click()
self.css_click('a.video_control.pause') EmptyPromise(lambda: self.is_paused, "Video is paused")
def _video_time(self): def _video_time(self):
""" """
Return a tuple `(elapsed_time, duration)`, each in seconds. Return a tuple `(elapsed_time, duration)`, each in seconds.
""" """
# The full time has the form "0:32 / 3:14" # The full time has the form "0:32 / 3:14"
all_times = self.css_text('div.vidtime') all_times = self.q(css='div.vidtime').text
if len(all_times) == 0: if len(all_times) == 0:
self.warning('Could not find video time') self.warning('Could not find video time')
...@@ -82,7 +82,7 @@ class VideoPage(PageObject): ...@@ -82,7 +82,7 @@ class VideoPage(PageObject):
elapsed_str, duration_str = full_time.split(' / ') elapsed_str, duration_str = full_time.split(' / ')
# Convert each string to seconds # Convert each string to seconds
return (self._parse_time_str(elapsed_str), self._parse_time_str(duration_str)) return self._parse_time_str(elapsed_str), self._parse_time_str(duration_str)
def _parse_time_str(self, time_str): def _parse_time_str(self, time_str):
""" """
......
...@@ -13,4 +13,4 @@ class AssetIndexPage(CoursePage): ...@@ -13,4 +13,4 @@ class AssetIndexPage(CoursePage):
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.q(css='body.view-uploads').present
...@@ -68,6 +68,6 @@ class AutoAuthPage(PageObject): ...@@ -68,6 +68,6 @@ class AutoAuthPage(PageObject):
return True return True
def get_user_id(self): def get_user_id(self):
message = self.css_text('BODY')[0].strip() message = self.q(css='BODY').text[0].strip()
match = re.search(r' user_id ([^$]+)$', message) match = re.search(r' user_id ([^$]+)$', message)
return match.groups()[0] if match else None return match.groups()[0] if match else None
...@@ -13,4 +13,4 @@ class ChecklistsPage(CoursePage): ...@@ -13,4 +13,4 @@ class ChecklistsPage(CoursePage):
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.q(css='body.view-checklists').present
...@@ -24,8 +24,9 @@ class ContainerPage(PageObject): ...@@ -24,8 +24,9 @@ class ContainerPage(PageObject):
def is_browser_on_page(self): def is_browser_on_page(self):
# Wait until all components have been loaded # Wait until all components have been loaded
return ( return (
self.is_css_present('body.view-container') and self.q(css='body.view-container').present and
len(self.q(css=XBlockWrapper.BODY_SELECTOR)) == len(self.q(css='{} .xblock'.format(XBlockWrapper.BODY_SELECTOR))) len(self.q(css=XBlockWrapper.BODY_SELECTOR).results) == len(
self.q(css='{} .xblock'.format(XBlockWrapper.BODY_SELECTOR)).results)
) )
@property @property
...@@ -33,7 +34,8 @@ class ContainerPage(PageObject): ...@@ -33,7 +34,8 @@ class ContainerPage(PageObject):
""" """
Return a list of xblocks loaded on the container page. Return a list of xblocks loaded on the container page.
""" """
return self.q(css=XBlockWrapper.BODY_SELECTOR).map(lambda el: XBlockWrapper(self.browser, el['data-locator'])).results return self.q(css=XBlockWrapper.BODY_SELECTOR).map(
lambda el: XBlockWrapper(self.browser, el.get_attribute('data-locator'))).results
class XBlockWrapper(PageObject): class XBlockWrapper(PageObject):
...@@ -49,7 +51,7 @@ class XBlockWrapper(PageObject): ...@@ -49,7 +51,7 @@ class XBlockWrapper(PageObject):
self.locator = locator self.locator = locator
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('{}[data-locator="{}"]'.format(self.BODY_SELECTOR, self.locator)) return self.q(css='{}[data-locator="{}"]'.format(self.BODY_SELECTOR, self.locator)).present
def _bounded_selector(self, selector): def _bounded_selector(self, selector):
""" """
...@@ -63,7 +65,7 @@ class XBlockWrapper(PageObject): ...@@ -63,7 +65,7 @@ class XBlockWrapper(PageObject):
@property @property
def name(self): def name(self):
titles = self.css_text(self._bounded_selector(self.NAME_SELECTOR)) titles = self.q(css=self._bounded_selector(self.NAME_SELECTOR)).text
if titles: if titles:
return titles[0] return titles[0]
else: else:
......
...@@ -13,4 +13,4 @@ class ImportPage(CoursePage): ...@@ -13,4 +13,4 @@ class ImportPage(CoursePage):
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.q(css='body.view-import').present
...@@ -13,4 +13,4 @@ class CourseUpdatesPage(CoursePage): ...@@ -13,4 +13,4 @@ class CourseUpdatesPage(CoursePage):
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.q(css='body.view-updates').present
...@@ -11,4 +11,4 @@ class SubsectionPage(PageObject): ...@@ -11,4 +11,4 @@ class SubsectionPage(PageObject):
""" """
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-subsection') return self.q(css='body.view-subsection').present
...@@ -13,4 +13,4 @@ class StaticPagesPage(CoursePage): ...@@ -13,4 +13,4 @@ class StaticPagesPage(CoursePage):
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.q(css='body.view-static-pages').present
...@@ -13,4 +13,4 @@ class ExportPage(CoursePage): ...@@ -13,4 +13,4 @@ class ExportPage(CoursePage):
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.q(css='body.view-export').present
...@@ -14,4 +14,4 @@ class HowitworksPage(PageObject): ...@@ -14,4 +14,4 @@ class HowitworksPage(PageObject):
url = BASE_URL + "/howitworks" url = BASE_URL + "/howitworks"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-howitworks') return self.q(css='body.view-howitworks').present
...@@ -14,4 +14,4 @@ class DashboardPage(PageObject): ...@@ -14,4 +14,4 @@ class DashboardPage(PageObject):
url = BASE_URL + "/course" url = BASE_URL + "/course"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-dashboard') return self.q(css='body.view-dashboard').present
...@@ -3,7 +3,7 @@ Login page for Studio. ...@@ -3,7 +3,7 @@ Login page for Studio.
""" """
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise, fulfill_after from bok_choy.promise import EmptyPromise
from . import BASE_URL from . import BASE_URL
...@@ -15,20 +15,19 @@ class LoginPage(PageObject): ...@@ -15,20 +15,19 @@ class LoginPage(PageObject):
url = BASE_URL + "/signin" url = BASE_URL + "/signin"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-signin') return self.q(css='body.view-signin').present
def login(self, email, password): def login(self, email, password):
""" """
Attempt to log in using `email` and `password`. Attempt to log in using `email` and `password`.
""" """
self.q(css='input#email').fill(email)
self.q(css='input#password').fill(password)
self.q(css='button#submit').first.click()
# Ensure that we make it to another page # Ensure that we make it to another page
on_next_page = EmptyPromise( EmptyPromise(
lambda: "login" not in self.browser.url, lambda: "login" not in self.browser.url,
"redirected from the login page" "redirected from the login page"
) ).fulfill()
with fulfill_after(on_next_page):
self.css_fill('input#email', email)
self.css_fill('input#password', password)
self.css_click('button#submit')
...@@ -13,4 +13,4 @@ class CourseTeamPage(CoursePage): ...@@ -13,4 +13,4 @@ class CourseTeamPage(CoursePage):
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.q(css='body.view-team').present
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
Course Outline page in Studio. Course Outline page in Studio.
""" """
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from bok_choy.query import SubQuery from bok_choy.promise import EmptyPromise
from bok_choy.promise import EmptyPromise, fulfill
from .course_page import CoursePage from .course_page import CoursePage
from .unit import UnitPage from .unit import UnitPage
class CourseOutlineContainer(object): class CourseOutlineContainer(object):
""" """
A mixin to a CourseOutline page object that adds the ability to load A mixin to a CourseOutline page object that adds the ability to load
...@@ -18,13 +18,19 @@ class CourseOutlineContainer(object): ...@@ -18,13 +18,19 @@ class CourseOutlineContainer(object):
CHILD_CLASS = None CHILD_CLASS = None
def child(self, title, child_class=None): def child(self, title, child_class=None):
"""
:type self: object
"""
if not child_class: if not child_class:
child_class = self.CHILD_CLASS child_class = self.CHILD_CLASS
return child_class( return child_class(
self.browser, self.browser,
self.q(css=child_class.BODY_SELECTOR).filter( self.q(css=child_class.BODY_SELECTOR).filter(
SubQuery(css=child_class.NAME_SELECTOR).filter(text=title) lambda el: title in [inner.text for inner in
)[0]['data-locator'] el.find_elements_by_css_selector(child_class.NAME_SELECTOR)]
).attrs('data-locator')[0]
) )
...@@ -104,22 +110,24 @@ class CourseOutlineSubsection(CourseOutlineChild, CourseOutlineContainer): ...@@ -104,22 +110,24 @@ class CourseOutlineSubsection(CourseOutlineChild, CourseOutlineContainer):
""" """
Toggle the expansion of this subsection. Toggle the expansion of this subsection.
""" """
self.disable_jquery_animations() self.browser.execute_script("jQuery.fx.off = true;")
def subsection_expanded(): def subsection_expanded():
return all( return all(
self.q(css=self._bounded_selector('.new-unit-item')) self.q(css=self._bounded_selector('.new-unit-item'))
.map(lambda el: el.visible) .map(lambda el: el.is_displayed())
.results .results
) )
currently_expanded = subsection_expanded() currently_expanded = subsection_expanded()
self.css_click(self._bounded_selector('.expand-collapse')) self.q(css=self._bounded_selector('.expand-collapse')).first.click()
fulfill(EmptyPromise(
EmptyPromise(
lambda: subsection_expanded() != currently_expanded, lambda: subsection_expanded() != currently_expanded,
"Check that the subsection {} has been toggled".format(self.locator), "Check that the subsection {} has been toggled".format(self.locator)
)) ).fulfill()
return self return self
...@@ -147,7 +155,7 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): ...@@ -147,7 +155,7 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
CHILD_CLASS = CourseOutlineSection CHILD_CLASS = CourseOutlineSection
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-outline') return self.q(css='body.view-outline').present
def section(self, title): def section(self, title):
""" """
......
...@@ -13,4 +13,4 @@ class SettingsPage(CoursePage): ...@@ -13,4 +13,4 @@ class SettingsPage(CoursePage):
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.q(css='body.view-settings').present
...@@ -13,4 +13,4 @@ class AdvancedSettingsPage(CoursePage): ...@@ -13,4 +13,4 @@ class AdvancedSettingsPage(CoursePage):
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.q(css='body.advanced').present
...@@ -13,4 +13,4 @@ class GradingPage(CoursePage): ...@@ -13,4 +13,4 @@ class GradingPage(CoursePage):
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.q(css='body.grading').present
...@@ -10,4 +10,4 @@ class SignupPage(PageObject): ...@@ -10,4 +10,4 @@ class SignupPage(PageObject):
url = BASE_URL + "/signup" url = BASE_URL + "/signup"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-signup') return self.q(css='body.view-signup').present
...@@ -13,4 +13,4 @@ class TextbooksPage(CoursePage): ...@@ -13,4 +13,4 @@ class TextbooksPage(CoursePage):
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.q(css='body.view-textbooks').present
...@@ -3,8 +3,7 @@ Unit page in Studio ...@@ -3,8 +3,7 @@ Unit page in Studio
""" """
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from bok_choy.query import SubQuery from bok_choy.promise import EmptyPromise
from bok_choy.promise import EmptyPromise, fulfill
from . import BASE_URL from . import BASE_URL
from .container import ContainerPage from .container import ContainerPage
...@@ -26,11 +25,11 @@ class UnitPage(PageObject): ...@@ -26,11 +25,11 @@ class UnitPage(PageObject):
def is_browser_on_page(self): def is_browser_on_page(self):
# Wait until all components have been loaded # Wait until all components have been loaded
number_of_leaf_xblocks = len(self.q(css='{} .xblock-student_view'.format(Component.BODY_SELECTOR))) number_of_leaf_xblocks = len(self.q(css='{} .xblock-student_view'.format(Component.BODY_SELECTOR)).results)
number_of_container_xblocks = len(self.q(css='{} .wrapper-xblock'.format(Component.BODY_SELECTOR))) number_of_container_xblocks = len(self.q(css='{} .wrapper-xblock'.format(Component.BODY_SELECTOR)).results)
return ( return (
self.is_css_present('body.view-unit') and self.q(css='body.view-unit').present and
len(self.q(css=Component.BODY_SELECTOR)) == number_of_leaf_xblocks + number_of_container_xblocks len(self.q(css=Component.BODY_SELECTOR).results) == number_of_leaf_xblocks + number_of_container_xblocks
) )
@property @property
...@@ -38,21 +37,24 @@ class UnitPage(PageObject): ...@@ -38,21 +37,24 @@ class UnitPage(PageObject):
""" """
Return a list of components loaded on the unit page. Return a list of components loaded on the unit page.
""" """
return self.q(css=Component.BODY_SELECTOR).map(lambda el: Component(self.browser, el['data-locator'])).results return self.q(css=Component.BODY_SELECTOR).map(
lambda el: Component(self.browser, el.get_attribute('data-locator'))).results
def edit_draft(self): def edit_draft(self):
""" """
Started editing a draft of this unit. Started editing a draft of this unit.
""" """
fulfill(EmptyPromise( EmptyPromise(
lambda: self.q(css='.create-draft').present, lambda: self.q(css='.create-draft').present,
'Wait for edit draft link to be present' 'Wait for edit draft link to be present'
)) ).fulfill()
self.q(css='.create-draft').click()
fulfill(EmptyPromise( self.q(css='.create-draft').first.click()
EmptyPromise(
lambda: self.q(css='.editing-draft-alert').present, lambda: self.q(css='.editing-draft-alert').present,
'Wait for draft mode to be activated' 'Wait for draft mode to be activated'
)) ).fulfill()
class Component(PageObject): class Component(PageObject):
...@@ -69,7 +71,7 @@ class Component(PageObject): ...@@ -69,7 +71,7 @@ class Component(PageObject):
self.locator = locator self.locator = locator
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('{}[data-locator="{}"]'.format(self.BODY_SELECTOR, self.locator)) return self.q(css='{}[data-locator="{}"]'.format(self.BODY_SELECTOR, self.locator)).present
def _bounded_selector(self, selector): def _bounded_selector(self, selector):
""" """
...@@ -83,7 +85,7 @@ class Component(PageObject): ...@@ -83,7 +85,7 @@ class Component(PageObject):
@property @property
def name(self): def name(self):
titles = self.css_text(self._bounded_selector(self.NAME_SELECTOR)) titles = self.q(css=self._bounded_selector(self.NAME_SELECTOR)).text
if titles: if titles:
return titles[0] return titles[0]
else: else:
...@@ -94,15 +96,15 @@ class Component(PageObject): ...@@ -94,15 +96,15 @@ class Component(PageObject):
return self._bounded_selector('.xblock-student_view') return self._bounded_selector('.xblock-student_view')
def edit(self): def edit(self):
self.css_click(self._bounded_selector('.edit-button')) self.q(css=self._bounded_selector('.edit-button')).first.click()
fulfill(EmptyPromise( EmptyPromise(
lambda: all( lambda: all(
self.q(css=self._bounded_selector('.component-editor')) self.q(css=self._bounded_selector('.component-editor'))
.map(lambda el: el.visible) .map(lambda el: el.is_displayed())
.results .results),
),
"Verify that the editor for component {} has been expanded".format(self.locator) "Verify that the editor for component {} has been expanded".format(self.locator)
)) ).fulfill()
return self return self
@property @property
......
...@@ -3,8 +3,7 @@ PageObjects related to the AcidBlock ...@@ -3,8 +3,7 @@ PageObjects related to the AcidBlock
""" """
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise, BrokenPromise, fulfill from bok_choy.promise import EmptyPromise, BrokenPromise
class AcidView(PageObject): class AcidView(PageObject):
""" """
...@@ -15,7 +14,7 @@ class AcidView(PageObject): ...@@ -15,7 +14,7 @@ class AcidView(PageObject):
def __init__(self, browser, context_selector): def __init__(self, browser, context_selector):
""" """
Args: Args:
browser (splinter.browser.Browser): The browser that this page is loaded in. browser (selenium.webdriver): The Selenium-controlled browser that this page is loaded in.
context_selector (str): The selector that identifies where this :class:`.AcidBlock` view context_selector (str): The selector that identifies where this :class:`.AcidBlock` view
is on the page. is on the page.
""" """
...@@ -25,14 +24,17 @@ class AcidView(PageObject): ...@@ -25,14 +24,17 @@ class AcidView(PageObject):
self.context_selector = context_selector self.context_selector = context_selector
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('{}.xblock-initialized .acid-block'.format(self.context_selector)) return (
self.q(css='{} .acid-block'.format(self.context_selector)).present and
self.browser.execute_script("return $({!r}).data('initialized')".format(self.context_selector))
)
def test_passed(self, test_selector): def test_passed(self, test_selector):
""" """
Return whether a particular :class:`.AcidBlock` test passed. Return whether a particular :class:`.AcidBlock` test passed.
""" """
selector = '{} .acid-block {} .pass'.format(self.context_selector, test_selector) selector = '{} .acid-block {} .pass'.format(self.context_selector, test_selector)
return bool(self.q(css=selector).execute(try_interval=0.1, timeout=3)) return bool(self.q(css=selector).results)
def child_test_passed(self, test_selector): def child_test_passed(self, test_selector):
""" """
......
...@@ -161,8 +161,9 @@ class DiscussionCommentEditTest(UniqueCourseTest): ...@@ -161,8 +161,9 @@ class DiscussionCommentEditTest(UniqueCourseTest):
def edit_comment(self, page, comment_id): def edit_comment(self, page, comment_id):
page.start_comment_edit(comment_id) page.start_comment_edit(comment_id)
page.set_comment_editor_value(comment_id, "edited body") new_comment = "edited body"
page.submit_comment_edit(comment_id) page.set_comment_editor_value(comment_id, new_comment)
page.submit_comment_edit(comment_id, new_comment)
def test_edit_comment_as_student(self): def test_edit_comment_as_student(self):
self.setup_user() self.setup_user()
......
...@@ -3,17 +3,12 @@ ...@@ -3,17 +3,12 @@
E2E tests for the LMS. E2E tests for the LMS.
""" """
from unittest import skip, expectedFailure from unittest import skip
from bok_choy.web_app_test import WebAppTest
from bok_choy.promise import EmptyPromise, fulfill_before, fulfill, Promise
from .helpers import UniqueCourseTest, load_data_str from .helpers import UniqueCourseTest, load_data_str
from ..pages.studio.auto_auth import AutoAuthPage from ..pages.studio.auto_auth import AutoAuthPage
from ..pages.lms.login import LoginPage
from ..pages.lms.find_courses import FindCoursesPage from ..pages.lms.find_courses import FindCoursesPage
from ..pages.lms.course_about import CourseAboutPage from ..pages.lms.course_about import CourseAboutPage
from ..pages.lms.register import RegisterPage
from ..pages.lms.course_info import CourseInfoPage from ..pages.lms.course_info import CourseInfoPage
from ..pages.lms.tab_nav import TabNavPage from ..pages.lms.tab_nav import TabNavPage
from ..pages.lms.course_nav import CourseNavPage from ..pages.lms.course_nav import CourseNavPage
...@@ -69,36 +64,12 @@ class RegistrationTest(UniqueCourseTest): ...@@ -69,36 +64,12 @@ class RegistrationTest(UniqueCourseTest):
course_names = dashboard.available_courses course_names = dashboard.available_courses
self.assertIn(self.course_info['display_name'], course_names) self.assertIn(self.course_info['display_name'], course_names)
def assert_course_available(self, course_id):
# Occassionally this does not show up immediately,
# so we wait and try reloading the page
def _check_course_available():
available = self.find_courses_page.course_id_list
if course_id in available:
return True
else:
self.find_courses_page.visit()
return False
return fulfill(EmptyPromise(
_check_course_available,
"Found course {course_id} in the list of available courses".format(course_id=course_id),
try_limit=3, try_interval=2
))
class LanguageTest(UniqueCourseTest): class LanguageTest(UniqueCourseTest):
""" """
Tests that the change language functionality on the dashboard works Tests that the change language functionality on the dashboard works
""" """
@property
def _changed_lang_promise(self):
def _check_func():
text = self.dashboard_page.current_courses_text
return (len(text) > 0, text)
return Promise(_check_func, "language changed")
def setUp(self): def setUp(self):
""" """
Initiailize dashboard page Initiailize dashboard page
...@@ -116,18 +87,17 @@ class LanguageTest(UniqueCourseTest): ...@@ -116,18 +87,17 @@ class LanguageTest(UniqueCourseTest):
self.password = "testpass" self.password = "testpass"
self.email = "test@example.com" self.email = "test@example.com"
@skip("Flakey in its present form; re-enable when fixed")
def test_change_lang(self): def test_change_lang(self):
AutoAuthPage(self.browser, course_id=self.course_id).visit() AutoAuthPage(self.browser, course_id=self.course_id).visit()
self.dashboard_page.visit() self.dashboard_page.visit()
# Change language to Dummy Esperanto # Change language to Dummy Esperanto
self.dashboard_page.change_language(self.test_new_lang) self.dashboard_page.change_language(self.test_new_lang)
changed_text = fulfill(self._changed_lang_promise) changed_text = self.dashboard_page.current_courses_text
# We should see the dummy-language text on the page # We should see the dummy-language text on the page
self.assertIn(self.current_courses_text, changed_text) self.assertIn(self.current_courses_text, changed_text)
@skip("Flakey in its present form; re-enable when fixed")
def test_language_persists(self): def test_language_persists(self):
auto_auth_page = AutoAuthPage(self.browser, username=self.username, password=self.password, email=self.email, course_id=self.course_id) auto_auth_page = AutoAuthPage(self.browser, username=self.username, password=self.password, email=self.email, course_id=self.course_id)
auto_auth_page.visit() auto_auth_page.visit()
...@@ -137,14 +107,15 @@ class LanguageTest(UniqueCourseTest): ...@@ -137,14 +107,15 @@ class LanguageTest(UniqueCourseTest):
self.dashboard_page.change_language(self.test_new_lang) self.dashboard_page.change_language(self.test_new_lang)
# destroy session # destroy session
self.browser._cookie_manager.delete() self.browser.delete_all_cookies()
# log back in # log back in
auto_auth_page.visit() auto_auth_page.visit()
self.dashboard_page.visit() self.dashboard_page.visit()
changed_text = fulfill(self._changed_lang_promise) changed_text = self.dashboard_page.current_courses_text
# We should see the dummy-language text on the page # We should see the dummy-language text on the page
self.assertIn(self.current_courses_text, changed_text) self.assertIn(self.current_courses_text, changed_text)
...@@ -173,7 +144,7 @@ class HighLevelTabTest(UniqueCourseTest): ...@@ -173,7 +144,7 @@ class HighLevelTabTest(UniqueCourseTest):
) )
course_fix.add_update( course_fix.add_update(
CourseUpdateDesc(date='January 29, 2014', content='Test course update') CourseUpdateDesc(date='January 29, 2014', content='Test course update1')
) )
course_fix.add_handout('demoPDF.pdf') course_fix.add_handout('demoPDF.pdf')
...@@ -200,6 +171,7 @@ class HighLevelTabTest(UniqueCourseTest): ...@@ -200,6 +171,7 @@ class HighLevelTabTest(UniqueCourseTest):
""" """
Navigate to the course info page. Navigate to the course info page.
""" """
# Navigate to the course info page from the progress page # Navigate to the course info page from the progress page
self.progress_page.visit() self.progress_page.visit()
self.tab_nav.go_to_tab('Course Info') self.tab_nav.go_to_tab('Course Info')
...@@ -251,6 +223,7 @@ class HighLevelTabTest(UniqueCourseTest): ...@@ -251,6 +223,7 @@ class HighLevelTabTest(UniqueCourseTest):
'Test Section': ['Test Subsection'], 'Test Section': ['Test Subsection'],
'Test Section 2': ['Test Subsection 2', 'Test Subsection 3'] 'Test Section 2': ['Test Subsection 2', 'Test Subsection 3']
} }
actual_sections = self.course_nav.sections actual_sections = self.course_nav.sections
for section, subsections in EXPECTED_SECTIONS.iteritems(): for section, subsections in EXPECTED_SECTIONS.iteritems():
self.assertIn(section, actual_sections) self.assertIn(section, actual_sections)
...@@ -321,22 +294,22 @@ class VideoTest(UniqueCourseTest): ...@@ -321,22 +294,22 @@ class VideoTest(UniqueCourseTest):
# Now we should be playing # Now we should be playing
self.assertTrue(self.video.is_playing) self.assertTrue(self.video.is_playing)
# Commented the below EmptyPromise, will move to its page once this test is working and stable
# Also there is should be no Promise check in any test as this should be done in Page Object
# Wait for the video to load the duration # Wait for the video to load the duration
video_duration_loaded = EmptyPromise( # EmptyPromise(
lambda: self.video.duration > 0, # lambda: self.video.duration > 0,
'video has duration', timeout=20 # 'video has duration', timeout=20
) # ).fulfill()
with fulfill_before(video_duration_loaded):
# Pause the video # Pause the video
self.video.pause() self.video.pause()
# Expect that the elapsed time and duration are reasonable # Expect that the elapsed time and duration are reasonable
# Again, we can't expect the video to actually play because of # Again, we can't expect the video to actually play because of
# latency through the ssh tunnel # latency through the ssh tunnel
self.assertGreaterEqual(self.video.elapsed_time, 0) self.assertGreaterEqual(self.video.elapsed_time, 0)
self.assertGreaterEqual(self.video.duration, self.video.elapsed_time) self.assertGreaterEqual(self.video.duration, self.video.elapsed_time)
class XBlockAcidBase(UniqueCourseTest): class XBlockAcidBase(UniqueCourseTest):
...@@ -441,7 +414,6 @@ class XBlockAcidChildTest(XBlockAcidBase): ...@@ -441,7 +414,6 @@ class XBlockAcidChildTest(XBlockAcidBase):
super(XBlockAcidChildTest, self).validate_acid_block_view() super(XBlockAcidChildTest, self).validate_acid_block_view()
self.assertTrue(acid_block.child_tests_passed) self.assertTrue(acid_block.child_tests_passed)
# This will fail until we fix support of children in pure XBlocks @skip('This will fail until we fix support of children in pure XBlocks')
@expectedFailure
def test_acid_block(self): def test_acid_block(self):
super(XBlockAcidChildTest, self).test_acid_block() super(XBlockAcidChildTest, self).test_acid_block()
...@@ -3,7 +3,8 @@ Tests for ORA (Open Response Assessment) through the LMS UI. ...@@ -3,7 +3,8 @@ Tests for ORA (Open Response Assessment) through the LMS UI.
""" """
import json import json
from bok_choy.promise import fulfill, Promise, BrokenPromise from bok_choy.promise import Promise, BrokenPromise
from ..pages.lms.peer_confirm import PeerConfirmPage
from ..pages.studio.auto_auth import AutoAuthPage from ..pages.studio.auto_auth import AutoAuthPage
from ..pages.lms.course_info import CourseInfoPage from ..pages.lms.course_info import CourseInfoPage
from ..pages.lms.tab_nav import TabNavPage from ..pages.lms.tab_nav import TabNavPage
...@@ -11,7 +12,7 @@ from ..pages.lms.course_nav import CourseNavPage ...@@ -11,7 +12,7 @@ from ..pages.lms.course_nav import CourseNavPage
from ..pages.lms.open_response import OpenResponsePage from ..pages.lms.open_response import OpenResponsePage
from ..pages.lms.peer_grade import PeerGradePage from ..pages.lms.peer_grade import PeerGradePage
from ..pages.lms.peer_calibrate import PeerCalibratePage from ..pages.lms.peer_calibrate import PeerCalibratePage
from ..pages.lms.peer_confirm import PeerConfirmPage
from ..pages.lms.progress import ProgressPage from ..pages.lms.progress import ProgressPage
from ..fixtures.course import XBlockFixtureDesc, CourseFixture from ..fixtures.course import XBlockFixtureDesc, CourseFixture
from ..fixtures.xqueue import XQueueResponseFixture from ..fixtures.xqueue import XQueueResponseFixture
...@@ -123,12 +124,11 @@ class OpenResponseTest(UniqueCourseTest): ...@@ -123,12 +124,11 @@ class OpenResponseTest(UniqueCourseTest):
# Because the check function involves fairly complicated actions # Because the check function involves fairly complicated actions
# (navigating through several screens), we give it more time to complete # (navigating through several screens), we give it more time to complete
# than the default. # than the default.
feedback_promise = Promise( return Promise(
self._check_feedback_func(assessment_type), self._check_feedback_func(assessment_type),
'Got feedback for {0} problem'.format(assessment_type), 'Got feedback for {0} problem'.format(assessment_type),
timeout=600, try_interval=5 timeout=600, try_interval=5
) ).fulfill()
return fulfill(feedback_promise)
def _check_feedback_func(self, assessment_type): def _check_feedback_func(self, assessment_type):
""" """
...@@ -155,11 +155,11 @@ class OpenResponseTest(UniqueCourseTest): ...@@ -155,11 +155,11 @@ class OpenResponseTest(UniqueCourseTest):
# Unsuccessful if the rubric hasn't loaded # Unsuccessful if the rubric hasn't loaded
except BrokenPromise: except BrokenPromise:
return (False, None) return False, None
# Successful if `feedback` is a non-empty list # Successful if `feedback` is a non-empty list
else: else:
return (bool(feedback), feedback) return bool(feedback), feedback
return _inner_check return _inner_check
...@@ -183,9 +183,11 @@ class SelfAssessmentTest(OpenResponseTest): ...@@ -183,9 +183,11 @@ class SelfAssessmentTest(OpenResponseTest):
# Fill in the rubric and expect that we get feedback # Fill in the rubric and expect that we get feedback
rubric = self.open_response.rubric rubric = self.open_response.rubric
self.assertEqual(rubric.categories, ["Writing Applications", "Language Conventions"]) self.assertEqual(rubric.categories, ["Writing Applications", "Language Conventions"])
rubric.set_scores([0, 1]) rubric.set_scores([0, 1])
rubric.submit() rubric.submit('self')
self.assertEqual(rubric.feedback, ['incorrect', 'correct']) self.assertEqual(rubric.feedback, ['incorrect', 'correct'])
# Verify the progress page # Verify the progress page
...@@ -306,7 +308,7 @@ class PeerAssessmentTest(OpenResponseTest): ...@@ -306,7 +308,7 @@ class PeerAssessmentTest(OpenResponseTest):
""" """
Given I am viewing a peer-assessment problem Given I am viewing a peer-assessment problem
And the instructor has submitted enough example essays And the instructor has submitted enough example essays
When I submit submit acceptable scores for enough calibration essays When I submit acceptable scores for enough calibration essays
Then I am able to peer-grade other students' essays. Then I am able to peer-grade other students' essays.
Given I have submitted an essay for peer-assessment Given I have submitted an essay for peer-assessment
...@@ -339,7 +341,7 @@ class PeerAssessmentTest(OpenResponseTest): ...@@ -339,7 +341,7 @@ class PeerAssessmentTest(OpenResponseTest):
rubric = self.peer_calibrate.rubric rubric = self.peer_calibrate.rubric
self.assertEqual(rubric.categories, ["Writing Applications", "Language Conventions"]) self.assertEqual(rubric.categories, ["Writing Applications", "Language Conventions"])
rubric.set_scores([0, 1]) rubric.set_scores([0, 1])
rubric.submit() rubric.submit('peer')
self.peer_calibrate.continue_to_grading() self.peer_calibrate.continue_to_grading()
# Grade a peer # Grade a peer
......
""" """
Acceptance tests for Studio. Acceptance tests for Studio.
""" """
from unittest import expectedFailure from unittest import skip
from bok_choy.web_app_test import WebAppTest from bok_choy.web_app_test import WebAppTest
...@@ -25,7 +25,7 @@ from ..pages.studio.textbooks import TextbooksPage ...@@ -25,7 +25,7 @@ from ..pages.studio.textbooks import TextbooksPage
from ..pages.xblock.acid import AcidView from ..pages.xblock.acid import AcidView
from ..fixtures.course import CourseFixture, XBlockFixtureDesc from ..fixtures.course import CourseFixture, XBlockFixtureDesc
from .helpers import UniqueCourseTest from .helpers import UniqueCourseTest, load_data_str
class LoggedOutTest(WebAppTest): class LoggedOutTest(WebAppTest):
...@@ -237,8 +237,7 @@ class XBlockAcidParentBase(XBlockAcidBase): ...@@ -237,8 +237,7 @@ class XBlockAcidParentBase(XBlockAcidBase):
acid_block = AcidView(self.browser, container.xblocks[0].preview_selector) acid_block = AcidView(self.browser, container.xblocks[0].preview_selector)
self.validate_acid_block_preview(acid_block) self.validate_acid_block_preview(acid_block)
# This will fail until the container page supports editing @skip('This will fail until the container page supports editing')
@expectedFailure
def test_acid_block_editor(self): def test_acid_block_editor(self):
super(XBlockAcidParentBase, self).test_acid_block_editor() super(XBlockAcidParentBase, self).test_acid_block_editor()
...@@ -299,12 +298,10 @@ class XBlockAcidChildTest(XBlockAcidParentBase): ...@@ -299,12 +298,10 @@ class XBlockAcidChildTest(XBlockAcidParentBase):
) )
).install() ).install()
# This will fail until we fix support of children in pure XBlocks @skip('This will fail until we fix support of children in pure XBlocks')
@expectedFailure
def test_acid_block_preview(self): def test_acid_block_preview(self):
super(XBlockAcidChildTest, self).test_acid_block_preview() super(XBlockAcidChildTest, self).test_acid_block_preview()
# This will fail until we fix support of children in pure XBlocks @skip('This will fail until we fix support of children in pure XBlocks')
@expectedFailure
def test_acid_block_editor(self): def test_acid_block_editor(self):
super(XBlockAcidChildTest, self).test_acid_block_editor() super(XBlockAcidChildTest, self).test_acid_block_editor()
...@@ -120,7 +120,7 @@ pep8==1.4.5 ...@@ -120,7 +120,7 @@ pep8==1.4.5
pylint==0.28 pylint==0.28
python-subunit==0.0.16 python-subunit==0.0.16
rednose==0.3 rednose==0.3
selenium==2.34.0 selenium==2.39.0
splinter==0.5.4 splinter==0.5.4
testtools==0.9.34 testtools==0.9.34
......
...@@ -23,6 +23,6 @@ ...@@ -23,6 +23,6 @@
-e git+https://github.com/edx/js-test-tool.git@v0.1.5#egg=js_test_tool -e git+https://github.com/edx/js-test-tool.git@v0.1.5#egg=js_test_tool
-e git+https://github.com/edx/django-waffle.git@823a102e48#egg=django-waffle -e git+https://github.com/edx/django-waffle.git@823a102e48#egg=django-waffle
-e git+https://github.com/edx/event-tracking.git@f0211d702d#egg=event-tracking -e git+https://github.com/edx/event-tracking.git@f0211d702d#egg=event-tracking
-e git+https://github.com/edx/bok-choy.git@62de7b576a08f36cde5b030c52bccb1a2f3f8df1#egg=bok_choy -e git+https://github.com/edx/bok-choy.git@25a47b3bf87c503fc4996e52addac83b42ec6f38#egg=bok_choy
-e git+https://github.com/edx-solutions/django-splash.git@9965a53c269666a30bb4e2b3f6037c138aef2a55#egg=django-splash -e git+https://github.com/edx-solutions/django-splash.git@9965a53c269666a30bb4e2b3f6037c138aef2a55#egg=django-splash
-e git+https://github.com/edx/acid-block.git@459aff7b63db8f2c5decd1755706c1a64fb4ebb1#egg=acid-xblock -e git+https://github.com/edx/acid-block.git@459aff7b63db8f2c5decd1755706c1a64fb4ebb1#egg=acid-xblock
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