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):
url_path = "about"
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):
"""
......@@ -22,7 +23,7 @@ class CourseAboutPage(CoursePage):
Waits for the registration page to load, then
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.wait_for_page()
......
......@@ -13,18 +13,18 @@ class CourseInfoPage(CoursePage):
url_path = "info"
def is_browser_on_page(self):
return self.is_css_present('section.updates')
return self.q(css='section.updates').present
@property
def num_updates(self):
"""
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
def handout_links(self):
"""
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
import re
from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise, fulfill_after
from bok_choy.promise import EmptyPromise
class CourseNavPage(PageObject):
......@@ -15,7 +15,7 @@ class CourseNavPage(PageObject):
url = None
def is_browser_on_page(self):
return self.is_css_present('div.course-index')
return self.q(css='div.course-index').present
@property
def sections(self):
......@@ -38,9 +38,7 @@ class CourseNavPage(PageObject):
section_titles = self._section_titles()
# Get the section titles for each chapter
for sec_index in range(len(section_titles)):
sec_title = section_titles[sec_index]
for sec_index, sec_title in enumerate(section_titles):
if len(section_titles) < 1:
self.warning("Could not find subsections for '{0}'".format(sec_title))
......@@ -60,7 +58,7 @@ class CourseNavPage(PageObject):
['Chemical Bonds Video', 'Practice Problems', 'Homework']
"""
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):
"""
......@@ -73,7 +71,7 @@ class CourseNavPage(PageObject):
"""
# 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
try:
......@@ -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)
# 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)
self.css_click(section_css)
self.q(css=section_css).first.click()
# Get the subsection by index
try:
......@@ -101,8 +99,9 @@ class CourseNavPage(PageObject):
)
# Click the subsection and ensure that the page finishes reloading
with fulfill_after(self._on_section_promise(section_title, subsection_title)):
self.css_click(subsection_css)
self.q(css=subsection_css).first.click()
self._on_section_promise(section_title, subsection_title).fulfill()
def go_to_sequential(self, sequential_title):
"""
......@@ -126,14 +125,14 @@ class CourseNavPage(PageObject):
# Click on the sequence item at the correct index
# 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)
self.css_click(seq_css)
self.q(css=seq_css).first.click()
def _section_titles(self):
"""
Return a list of all section titles on the page.
"""
chapter_css = 'nav>div.chapter>h3>a'
return self.css_map(chapter_css, lambda el: el.text.strip())
chapter_css = 'nav > div.chapter > h3 > a'
return self.q(css=chapter_css).map(lambda el: el.text.strip()).results
def _subsection_titles(self, section_index):
"""
......@@ -148,10 +147,10 @@ class CourseNavPage(PageObject):
# Otherwise, we need to get the HTML
# It *would* make sense to always get the HTML, but unfortunately
# the open tab has some child <span> tags that we don't want.
return self.css_map(
subsection_css,
lambda el: el.text.strip().split('\n')[0] if el.visible else el.html.strip()
)
return self.q(
css=subsection_css).map(
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):
"""
......@@ -172,8 +171,8 @@ class CourseNavPage(PageObject):
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).
"""
current_section_list = self.css_text('nav>div.chapter.is-open>h3>a')
current_subsection_list = self.css_text('nav>div.chapter.is-open li.active>a>p')
current_section_list = self.q(css='nav>div.chapter.is-open>h3>a').text
current_subsection_list = self.q(css='nav>div.chapter.is-open li.active>a>p').text
if len(current_section_list) == 0:
self.warning("Could not find the current section")
......@@ -196,4 +195,4 @@ class CourseNavPage(PageObject):
"""
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.
"""
from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise
from . import BASE_URL
......@@ -16,11 +17,11 @@ class DashboardPage(PageObject):
url = BASE_URL + "/dashboard"
def is_browser_on_page(self):
return self.is_css_present('section.my-courses')
return self.q(css='section.my-courses').present
@property
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:
return text_items[0]
else:
......@@ -36,7 +37,7 @@ class DashboardPage(PageObject):
_, course_name = el.text.split(' ', 1)
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):
"""
......@@ -45,7 +46,7 @@ class DashboardPage(PageObject):
link_css = self._link_css(course_id)
if link_css is not None:
self.css_click(link_css)
self.q(css=link_css).first.click()
else:
msg = "No links found for course {0}".format(course_id)
self.warning(msg)
......@@ -55,7 +56,7 @@ class DashboardPage(PageObject):
Return a CSS selector for the link to the course with `course_id`.
"""
# 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
link_index = None
......@@ -73,6 +74,13 @@ class DashboardPage(PageObject):
"""
Change the language on the dashboard to the language corresponding with `code`.
"""
self.css_click(".edit-language")
self.select_option("language", code)
self.css_click("#submit-lang")
self.q(css=".edit-language").first.click()
self.q(css='select[name="language"] option[value="{}"]'.format(code)).first.click()
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.promise import EmptyPromise, fulfill
from bok_choy.promise import EmptyPromise
from .course_page import CoursePage
......@@ -10,9 +10,9 @@ class DiscussionSingleThreadPage(CoursePage):
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)
)
return self.q(
css="body.discussion .discussion-article[data-id='{thread_id}']".format(thread_id=self.thread_id)
).present
@property
@unguarded
......@@ -24,7 +24,7 @@ class DiscussionSingleThreadPage(CoursePage):
Returns the text of the first element matching the given selector, or
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
def get_response_total_text(self):
......@@ -33,7 +33,7 @@ class DiscussionSingleThreadPage(CoursePage):
def get_num_displayed_responses(self):
"""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):
"""Returns the shown response count text, or None if not present"""
......@@ -44,12 +44,16 @@ class DiscussionSingleThreadPage(CoursePage):
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"
))
"""Clicks the load more responses button and waits for responses to load"""
self.q(css=".load-response-button").first.click()
def _is_ajax_finished():
return self.browser.execute_script("return jQuery.active") == 0
EmptyPromise(
_is_ajax_finished,
"Loading more Responses"
).fulfill()
def has_add_response_button(self):
"""Returns true if the add response button is visible, false otherwise"""
......@@ -60,14 +64,17 @@ class DiscussionSingleThreadPage(CoursePage):
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)),
self.q(css=".add-response-btn").first.click()
EmptyPromise(
lambda: self.q(css="#wmd-input-reply-body-{thread_id}:focus".format(thread_id=self.thread_id)),
"Response field received focus"
))
).fulfill()
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):
"""Returns true if the response editor is present, false otherwise"""
......@@ -75,15 +82,15 @@ class DiscussionSingleThreadPage(CoursePage):
def start_response_edit(self, response_id):
"""Click the edit button for the response, loading the editing view"""
self.css_click(".response_{} .discussion-response .action-edit".format(response_id))
fulfill(EmptyPromise(
self.q(css=".response_{} .discussion-response .action-edit".format(response_id)).first.click()
EmptyPromise(
lambda: self.is_response_editor_visible(response_id),
"Response edit started"
))
).fulfill()
def is_add_comment_visible(self, response_id):
"""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):
"""Returns true if the comment is viewable onscreen"""
......@@ -98,11 +105,11 @@ class DiscussionSingleThreadPage(CoursePage):
def delete_comment(self, comment_id):
with self.handle_alert():
self.css_click("#comment_{} div.action-delete".format(comment_id))
fulfill(EmptyPromise(
self.q(css="#comment_{} div.action-delete".format(comment_id)).first.click()
EmptyPromise(
lambda: not self.is_comment_visible(comment_id),
"Deleted comment was removed"
))
).fulfill()
def is_comment_editable(self, comment_id):
"""Returns true if the edit comment button is present, false otherwise"""
......@@ -110,49 +117,48 @@ class DiscussionSingleThreadPage(CoursePage):
def is_comment_editor_visible(self, comment_id):
"""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):
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):
"""Click the edit button for the comment, loading the editing view"""
old_body = self.get_comment_body(comment_id)
self.css_click("#comment_{} .action-edit".format(comment_id))
fulfill(EmptyPromise(
self.q(css="#comment_{} .action-edit".format(comment_id)).first.click()
EmptyPromise(
lambda: (
self.is_comment_editor_visible(comment_id) and
not self.is_comment_visible(comment_id) and
self._get_comment_editor_value(comment_id) == old_body
),
"Comment edit started"
))
).fulfill()
def set_comment_editor_value(self, comment_id, new_body):
"""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"""
new_body = self._get_comment_editor_value(comment_id)
self.css_click("#comment_{} .post-update".format(comment_id))
fulfill(EmptyPromise(
self.q(css="#comment_{} .post-update".format(comment_id)).first.click()
EmptyPromise(
lambda: (
not self.is_comment_editor_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"
))
).fulfill()
def cancel_comment_edit(self, comment_id, original_body):
"""Click the cancel button on the comment editor"""
self.css_click("#comment_{} .post-cancel".format(comment_id))
fulfill(EmptyPromise(
self.q(css="#comment_{} .post-cancel".format(comment_id)).first.click()
EmptyPromise(
lambda: (
not self.is_comment_editor_visible(comment_id) and
self.is_comment_visible(comment_id) and
self.get_comment_body(comment_id) == original_body
),
"Comment edit was canceled"
))
).fulfill()
......@@ -23,4 +23,5 @@ class FindCoursesPage(PageObject):
Retrieve the list of available course IDs
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.
"""
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
......@@ -17,13 +17,27 @@ class LoginPage(PageObject):
def is_browser_on_page(self):
return any([
'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):
"""
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
on_next_page = EmptyPromise(
lambda: "login" not in self.browser.url,
......@@ -34,3 +48,5 @@ class LoginPage(PageObject):
self.css_fill('input#email', email)
self.css_fill('input#password', password)
self.css_click('button#submit')
"""
......@@ -3,7 +3,7 @@ Open-ended response in the courseware.
"""
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
......@@ -15,7 +15,7 @@ class OpenResponsePage(PageObject):
url = None
def is_browser_on_page(self):
return self.is_css_present('div.xmodule_CombinedOpenEndedModule')
return self.q(css='div.xmodule_CombinedOpenEndedModule').present
@property
def assessment_type(self):
......@@ -23,7 +23,7 @@ class OpenResponsePage(PageObject):
Return the type of assessment currently active.
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:
self.warning("Could not find assessment type label")
......@@ -46,7 +46,7 @@ class OpenResponsePage(PageObject):
Return an HTML string representing the essay 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:
self.warning("Could not find essay prompt on page.")
......@@ -73,7 +73,7 @@ class OpenResponsePage(PageObject):
Return the written feedback from the grader (if any).
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:
return feedback[0]
......@@ -85,7 +85,7 @@ class OpenResponsePage(PageObject):
"""
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:
return ""
......@@ -98,7 +98,7 @@ class OpenResponsePage(PageObject):
Status message from the grader.
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:
self.warning("No grader status found")
......@@ -114,27 +114,26 @@ class OpenResponsePage(PageObject):
Input a response to the prompt.
"""
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):
"""
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(),
"Status message saved"
)
with fulfill_after(status_msg_shown):
self.css_click('input.save-button')
).fulfill()
def submit_response(self):
"""
Submit a response for grading.
"""
self.css_click('input.submit-button')
self.q(css='input.submit-button').first.click()
# modal dialog confirmation
self.css_click('button.ok-button')
self.q(css='button.ok-button').first.click()
# Ensure that the submission completes
self._wait_for_submitted(self.assessment_type)
......@@ -148,11 +147,11 @@ class OpenResponsePage(PageObject):
RubricPage(self.browser).wait_for_page()
elif assessment_type == 'ai' or assessment_type == "peer":
fulfill(EmptyPromise(
EmptyPromise(
lambda: self.grader_status != 'Unanswered',
"Problem status is no longer 'unanswered'"
))
).fulfill()
else:
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
"""
from bok_choy.page_object import PageObject
from bok_choy.promise import Promise
from .rubric import RubricPage
......@@ -15,16 +16,21 @@ class PeerCalibratePage(PageObject):
url = None
def is_browser_on_page(self):
return (
self.is_css_present('div.peer-grading-tools') or
self.is_css_present('div.calibration-panel.current-state')
)
def _is_correct_page():
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):
"""
Continue to peer grading after completing calibration.
"""
self.css_click('input.calibration-feedback-button')
self.q(css='input.calibration-feedback-button').first.click()
@property
def rubric(self):
......@@ -33,7 +39,7 @@ class PeerCalibratePage(PageObject):
If no rubric is available, raises a `BrokenPromise` exception.
"""
rubric = RubricPage(self.browser)
rubric.wait_for_page()
rubric.wait_for_page(timeout=60)
return rubric
@property
......@@ -41,7 +47,7 @@ class PeerCalibratePage(PageObject):
"""
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:
return None
else:
......
......@@ -3,6 +3,7 @@ Confirmation screen for peer calibration and grading.
"""
from bok_choy.page_object import PageObject
from bok_choy.promise import Promise
class PeerConfirmPage(PageObject):
......@@ -13,7 +14,12 @@ class PeerConfirmPage(PageObject):
url = None
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):
"""
......@@ -21,7 +27,6 @@ class PeerConfirmPage(PageObject):
If `is_calibrating` is false, try to continue to peer grading.
Otherwise, try to continue to calibration grading.
"""
self.css_click(
'input.calibration-interstitial-page-button'
self.q(css='input.calibration-interstitial-page-button'
if is_calibrating else 'input.interstitial-page-button'
)
).first.click()
......@@ -3,6 +3,7 @@ Students grade peer submissions.
"""
from bok_choy.page_object import PageObject
from bok_choy.promise import Promise
from .rubric import RubricPage
......@@ -14,24 +15,28 @@ class PeerGradePage(PageObject):
url = None
def is_browser_on_page(self):
return (
self.is_css_present('div.peer-grading-tools') or
self.is_css_present('div.grading-panel.current-state')
)
def _is_correct_page():
is_present = (
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
def problem_list(self):
"""
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):
"""
Choose the problem with `problem_name` to start grading or calibrating.
"""
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
def rubric(self):
......
......@@ -12,9 +12,10 @@ class ProgressPage(CoursePage):
url_path = "progress"
#@property
def is_browser_on_page(self):
has_course_info = self.is_css_present('div.course-info')
has_graph = self.is_css_present('div#grade-detail-graph')
has_course_info = self.q(css='div.course-info').present
has_graph = self.q(css='div#grade-detail-graph').present
return has_course_info and has_graph
def scores(self, chapter, section):
......@@ -46,7 +47,7 @@ class ProgressPage(CoursePage):
Returns `None` if it cannot find such a chapter.
"""
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:
# CSS indices are 1-indexed, so add one to the list index
......@@ -65,7 +66,7 @@ class ProgressPage(CoursePage):
# Get the links containing the section titles in `chapter_index`.
# 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_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
# We have to remove this to find the right title
......@@ -95,7 +96,7 @@ class ProgressPage(CoursePage):
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)
return [tuple(map(int, score.split('/'))) for score in text_scores]
......@@ -34,7 +34,7 @@ class RegisterPage(PageObject):
def is_browser_on_page(self):
return any([
'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):
......@@ -42,18 +42,18 @@ class RegisterPage(PageObject):
Fill in registration info.
`email`, `password`, `username`, and `full_name` are the user's credentials.
"""
self.css_fill('input#email', email)
self.css_fill('input#password', password)
self.css_fill('input#username', username)
self.css_fill('input#name', full_name)
self.css_check('input#tos-yes')
self.css_check('input#honorcode-yes')
self.q(css='input#email').fill(email)
self.q(css='input#password').fill(password)
self.q(css='input#username').fill(username)
self.q(css='input#name').fill(full_name)
self.q(css='input#tos-yes').first.click()
self.q(css='input#honorcode-yes').first.click()
def submit(self):
"""
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
dashboard = DashboardPage(self.browser)
......
......@@ -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.promise import EmptyPromise, fulfill_after, fulfill_before
from bok_choy.promise import EmptyPromise
class ScoreMismatchError(Exception):
......@@ -24,7 +24,7 @@ class RubricPage(PageObject):
"""
Return a boolean indicating whether the rubric is available.
"""
return self.is_css_present('div.rubric')
return self.q(css='div.rubric').present
@property
def categories(self):
......@@ -37,7 +37,7 @@ class RubricPage(PageObject):
The rubric is not always visible; if it's not available,
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):
"""
......@@ -60,10 +60,9 @@ class RubricPage(PageObject):
# Set the score for each category
for score_index in range(len(scores)):
# Check that we have the enough radio buttons
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(
"Tried to select score {0} but there are only {1} options".format(
score_index, len(scores)))
......@@ -74,7 +73,12 @@ class RubricPage(PageObject):
category_css +
">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
def feedback(self):
......@@ -86,14 +90,13 @@ class RubricPage(PageObject):
If feedback could not be interpreted (unexpected CSS class),
the list will contain a `None` item.
"""
# Get the green checkmark / red x labels
# We need to filter out the similar-looking CSS classes
# for the rubric items that are NOT marked correct/incorrect
feedback_css = 'div.rubric-label>label'
labels = [
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'
]
......@@ -110,17 +113,29 @@ class RubricPage(PageObject):
return map(map_feedback, labels)
def submit(self):
def submit(self, promise_check_type=None):
"""
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
button_css = 'input.submit-button'
button_enabled = EmptyPromise(
lambda: all(self.css_map(button_css, lambda el: not el['disabled'])),
"Submit button enabled"
)
EmptyPromise(
lambda: all(self.q(css=button_css).map(lambda el: not el.get_attribute('disabled')).results),
"Submit button not enabled"
).fulfill()
# Submit the assessment
with fulfill_before(button_enabled):
self.css_click(button_css)
self.q(css=button_css).first.click()
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.
"""
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):
......@@ -14,12 +14,13 @@ class TabNavPage(PageObject):
url = None
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):
"""
Navigate to the tab `tab_name`.
"""
if tab_name not in ['Courseware', 'Course Info', 'Discussion', 'Wiki', 'Progress']:
self.warning("'{0}' is not a valid tab name".format(tab_name))
......@@ -27,11 +28,12 @@ class TabNavPage(PageObject):
# so we find the tab with `tab_name` in its text.
tab_css = self._tab_css(tab_name)
with fulfill_after(self._is_on_tab_promise(tab_name)):
if tab_css is not None:
self.css_click(tab_css)
else:
self.warning("No tabs found for '{0}'".format(tab_name))
if tab_css is not None:
self.q(css=tab_css).first.click()
else:
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):
"""
......@@ -63,10 +65,10 @@ class TabNavPage(PageObject):
if the tab names fail to load.
"""
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 fulfill(Promise(_check_func, "Get all tab names"))
return Promise(_check_func, "Get all tab names").fulfill()
def _is_on_tab(self, tab_name):
"""
......@@ -74,14 +76,13 @@ class TabNavPage(PageObject):
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.
"""
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:
self.warning("Could not find current tab")
return False
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):
......
......@@ -4,7 +4,7 @@ Video player in the courseware.
import time
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
......@@ -17,7 +17,7 @@ class VideoPage(PageObject):
url = None
def is_browser_on_page(self):
return self.is_css_present('div.xmodule_VideoModule')
return self.q(css='div.xmodule_VideoModule').present
@property
def elapsed_time(self):
......@@ -40,37 +40,37 @@ class VideoPage(PageObject):
"""
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
def is_paused(self):
"""
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
def play(self):
"""
Start playing the video.
"""
with fulfill_after(EmptyPromise(lambda: self.is_playing, "Video is playing")):
self.css_click('a.video_control.play')
self.q(css='a.video_control.play').first.click()
EmptyPromise(lambda: self.is_playing, "Video is playing")
@wait_for_js
def pause(self):
"""
Pause the video.
"""
with fulfill_after(EmptyPromise(lambda: self.is_paused, "Video is paused")):
self.css_click('a.video_control.pause')
self.q(css='a.video_control.pause').first.click()
EmptyPromise(lambda: self.is_paused, "Video is paused")
def _video_time(self):
"""
Return a tuple `(elapsed_time, duration)`, each in seconds.
"""
# 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:
self.warning('Could not find video time')
......@@ -82,7 +82,7 @@ class VideoPage(PageObject):
elapsed_str, duration_str = full_time.split(' / ')
# 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):
"""
......
......@@ -13,4 +13,4 @@ class AssetIndexPage(CoursePage):
url_path = "assets"
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):
return True
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)
return match.groups()[0] if match else None
......@@ -13,4 +13,4 @@ class ChecklistsPage(CoursePage):
url_path = "checklists"
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):
def is_browser_on_page(self):
# Wait until all components have been loaded
return (
self.is_css_present('body.view-container') and
len(self.q(css=XBlockWrapper.BODY_SELECTOR)) == len(self.q(css='{} .xblock'.format(XBlockWrapper.BODY_SELECTOR)))
self.q(css='body.view-container').present and
len(self.q(css=XBlockWrapper.BODY_SELECTOR).results) == len(
self.q(css='{} .xblock'.format(XBlockWrapper.BODY_SELECTOR)).results)
)
@property
......@@ -33,7 +34,8 @@ class ContainerPage(PageObject):
"""
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):
......@@ -49,7 +51,7 @@ class XBlockWrapper(PageObject):
self.locator = locator
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):
"""
......@@ -63,7 +65,7 @@ class XBlockWrapper(PageObject):
@property
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:
return titles[0]
else:
......
......@@ -13,4 +13,4 @@ class ImportPage(CoursePage):
url_path = "import"
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):
url_path = "course_info"
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):
"""
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):
url_path = "tabs"
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):
url_path = "export"
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):
url = BASE_URL + "/howitworks"
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):
url = BASE_URL + "/course"
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.
"""
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
......@@ -15,20 +15,19 @@ class LoginPage(PageObject):
url = BASE_URL + "/signin"
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):
"""
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
on_next_page = EmptyPromise(
EmptyPromise(
lambda: "login" not in self.browser.url,
"redirected from the login page"
)
with fulfill_after(on_next_page):
self.css_fill('input#email', email)
self.css_fill('input#password', password)
self.css_click('button#submit')
).fulfill()
......@@ -13,4 +13,4 @@ class CourseTeamPage(CoursePage):
url_path = "course_team"
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 @@
Course Outline page in Studio.
"""
from bok_choy.page_object import PageObject
from bok_choy.query import SubQuery
from bok_choy.promise import EmptyPromise, fulfill
from bok_choy.promise import EmptyPromise
from .course_page import CoursePage
from .unit import UnitPage
class CourseOutlineContainer(object):
"""
A mixin to a CourseOutline page object that adds the ability to load
......@@ -18,13 +18,19 @@ class CourseOutlineContainer(object):
CHILD_CLASS = None
def child(self, title, child_class=None):
"""
:type self: object
"""
if not child_class:
child_class = self.CHILD_CLASS
return child_class(
self.browser,
self.q(css=child_class.BODY_SELECTOR).filter(
SubQuery(css=child_class.NAME_SELECTOR).filter(text=title)
)[0]['data-locator']
lambda el: title in [inner.text for inner in
el.find_elements_by_css_selector(child_class.NAME_SELECTOR)]
).attrs('data-locator')[0]
)
......@@ -104,22 +110,24 @@ class CourseOutlineSubsection(CourseOutlineChild, CourseOutlineContainer):
"""
Toggle the expansion of this subsection.
"""
self.disable_jquery_animations()
self.browser.execute_script("jQuery.fx.off = true;")
def subsection_expanded():
return all(
self.q(css=self._bounded_selector('.new-unit-item'))
.map(lambda el: el.visible)
.results
.map(lambda el: el.is_displayed())
.results
)
currently_expanded = subsection_expanded()
self.css_click(self._bounded_selector('.expand-collapse'))
fulfill(EmptyPromise(
self.q(css=self._bounded_selector('.expand-collapse')).first.click()
EmptyPromise(
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
......@@ -147,7 +155,7 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
CHILD_CLASS = CourseOutlineSection
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):
"""
......
......@@ -13,4 +13,4 @@ class SettingsPage(CoursePage):
url_path = "settings/details"
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):
url_path = "settings/advanced"
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):
url_path = "settings/grading"
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):
url = BASE_URL + "/signup"
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):
url_path = "textbooks"
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
"""
from bok_choy.page_object import PageObject
from bok_choy.query import SubQuery
from bok_choy.promise import EmptyPromise, fulfill
from bok_choy.promise import EmptyPromise
from . import BASE_URL
from .container import ContainerPage
......@@ -26,11 +25,11 @@ class UnitPage(PageObject):
def is_browser_on_page(self):
# Wait until all components have been loaded
number_of_leaf_xblocks = len(self.q(css='{} .xblock-student_view'.format(Component.BODY_SELECTOR)))
number_of_container_xblocks = len(self.q(css='{} .wrapper-xblock'.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)).results)
return (
self.is_css_present('body.view-unit') and
len(self.q(css=Component.BODY_SELECTOR)) == number_of_leaf_xblocks + number_of_container_xblocks
self.q(css='body.view-unit').present and
len(self.q(css=Component.BODY_SELECTOR).results) == number_of_leaf_xblocks + number_of_container_xblocks
)
@property
......@@ -38,21 +37,24 @@ class UnitPage(PageObject):
"""
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):
"""
Started editing a draft of this unit.
"""
fulfill(EmptyPromise(
EmptyPromise(
lambda: self.q(css='.create-draft').present,
'Wait for edit draft link to be present'
))
self.q(css='.create-draft').click()
fulfill(EmptyPromise(
).fulfill()
self.q(css='.create-draft').first.click()
EmptyPromise(
lambda: self.q(css='.editing-draft-alert').present,
'Wait for draft mode to be activated'
))
).fulfill()
class Component(PageObject):
......@@ -69,7 +71,7 @@ class Component(PageObject):
self.locator = locator
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):
"""
......@@ -83,7 +85,7 @@ class Component(PageObject):
@property
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:
return titles[0]
else:
......@@ -94,15 +96,15 @@ class Component(PageObject):
return self._bounded_selector('.xblock-student_view')
def edit(self):
self.css_click(self._bounded_selector('.edit-button'))
fulfill(EmptyPromise(
self.q(css=self._bounded_selector('.edit-button')).first.click()
EmptyPromise(
lambda: all(
self.q(css=self._bounded_selector('.component-editor'))
.map(lambda el: el.visible)
.results
),
.map(lambda el: el.is_displayed())
.results),
"Verify that the editor for component {} has been expanded".format(self.locator)
))
).fulfill()
return self
@property
......
......@@ -3,8 +3,7 @@ PageObjects related to the AcidBlock
"""
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):
"""
......@@ -15,7 +14,7 @@ class AcidView(PageObject):
def __init__(self, browser, context_selector):
"""
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
is on the page.
"""
......@@ -25,14 +24,17 @@ class AcidView(PageObject):
self.context_selector = context_selector
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):
"""
Return whether a particular :class:`.AcidBlock` test passed.
"""
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):
"""
......
......@@ -161,8 +161,9 @@ class DiscussionCommentEditTest(UniqueCourseTest):
def edit_comment(self, page, comment_id):
page.start_comment_edit(comment_id)
page.set_comment_editor_value(comment_id, "edited body")
page.submit_comment_edit(comment_id)
new_comment = "edited body"
page.set_comment_editor_value(comment_id, new_comment)
page.submit_comment_edit(comment_id, new_comment)
def test_edit_comment_as_student(self):
self.setup_user()
......
......@@ -3,17 +3,12 @@
E2E tests for the LMS.
"""
from unittest import skip, expectedFailure
from bok_choy.web_app_test import WebAppTest
from bok_choy.promise import EmptyPromise, fulfill_before, fulfill, Promise
from unittest import skip
from .helpers import UniqueCourseTest, load_data_str
from ..pages.studio.auto_auth import AutoAuthPage
from ..pages.lms.login import LoginPage
from ..pages.lms.find_courses import FindCoursesPage
from ..pages.lms.course_about import CourseAboutPage
from ..pages.lms.register import RegisterPage
from ..pages.lms.course_info import CourseInfoPage
from ..pages.lms.tab_nav import TabNavPage
from ..pages.lms.course_nav import CourseNavPage
......@@ -69,36 +64,12 @@ class RegistrationTest(UniqueCourseTest):
course_names = dashboard.available_courses
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):
"""
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):
"""
Initiailize dashboard page
......@@ -116,18 +87,17 @@ class LanguageTest(UniqueCourseTest):
self.password = "testpass"
self.email = "test@example.com"
@skip("Flakey in its present form; re-enable when fixed")
def test_change_lang(self):
AutoAuthPage(self.browser, course_id=self.course_id).visit()
self.dashboard_page.visit()
# Change language to Dummy Esperanto
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
self.assertIn(self.current_courses_text, changed_text)
@skip("Flakey in its present form; re-enable when fixed")
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.visit()
......@@ -137,14 +107,15 @@ class LanguageTest(UniqueCourseTest):
self.dashboard_page.change_language(self.test_new_lang)
# destroy session
self.browser._cookie_manager.delete()
self.browser.delete_all_cookies()
# log back in
auto_auth_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
self.assertIn(self.current_courses_text, changed_text)
......@@ -173,7 +144,7 @@ class HighLevelTabTest(UniqueCourseTest):
)
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')
......@@ -200,6 +171,7 @@ class HighLevelTabTest(UniqueCourseTest):
"""
Navigate to the course info page.
"""
# Navigate to the course info page from the progress page
self.progress_page.visit()
self.tab_nav.go_to_tab('Course Info')
......@@ -251,6 +223,7 @@ class HighLevelTabTest(UniqueCourseTest):
'Test Section': ['Test Subsection'],
'Test Section 2': ['Test Subsection 2', 'Test Subsection 3']
}
actual_sections = self.course_nav.sections
for section, subsections in EXPECTED_SECTIONS.iteritems():
self.assertIn(section, actual_sections)
......@@ -321,22 +294,22 @@ class VideoTest(UniqueCourseTest):
# Now we should be 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
video_duration_loaded = EmptyPromise(
lambda: self.video.duration > 0,
'video has duration', timeout=20
)
with fulfill_before(video_duration_loaded):
# EmptyPromise(
# lambda: self.video.duration > 0,
# 'video has duration', timeout=20
# ).fulfill()
# Pause the video
self.video.pause()
# Pause the video
self.video.pause()
# Expect that the elapsed time and duration are reasonable
# Again, we can't expect the video to actually play because of
# latency through the ssh tunnel
self.assertGreaterEqual(self.video.elapsed_time, 0)
self.assertGreaterEqual(self.video.duration, self.video.elapsed_time)
# Expect that the elapsed time and duration are reasonable
# Again, we can't expect the video to actually play because of
# latency through the ssh tunnel
self.assertGreaterEqual(self.video.elapsed_time, 0)
self.assertGreaterEqual(self.video.duration, self.video.elapsed_time)
class XBlockAcidBase(UniqueCourseTest):
......@@ -441,7 +414,6 @@ class XBlockAcidChildTest(XBlockAcidBase):
super(XBlockAcidChildTest, self).validate_acid_block_view()
self.assertTrue(acid_block.child_tests_passed)
# This will fail until we fix support of children in pure XBlocks
@expectedFailure
@skip('This will fail until we fix support of children in pure XBlocks')
def test_acid_block(self):
super(XBlockAcidChildTest, self).test_acid_block()
......@@ -3,7 +3,8 @@ Tests for ORA (Open Response Assessment) through the LMS UI.
"""
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.lms.course_info import CourseInfoPage
from ..pages.lms.tab_nav import TabNavPage
......@@ -11,7 +12,7 @@ from ..pages.lms.course_nav import CourseNavPage
from ..pages.lms.open_response import OpenResponsePage
from ..pages.lms.peer_grade import PeerGradePage
from ..pages.lms.peer_calibrate import PeerCalibratePage
from ..pages.lms.peer_confirm import PeerConfirmPage
from ..pages.lms.progress import ProgressPage
from ..fixtures.course import XBlockFixtureDesc, CourseFixture
from ..fixtures.xqueue import XQueueResponseFixture
......@@ -123,12 +124,11 @@ class OpenResponseTest(UniqueCourseTest):
# Because the check function involves fairly complicated actions
# (navigating through several screens), we give it more time to complete
# than the default.
feedback_promise = Promise(
return Promise(
self._check_feedback_func(assessment_type),
'Got feedback for {0} problem'.format(assessment_type),
timeout=600, try_interval=5
)
return fulfill(feedback_promise)
).fulfill()
def _check_feedback_func(self, assessment_type):
"""
......@@ -155,11 +155,11 @@ class OpenResponseTest(UniqueCourseTest):
# Unsuccessful if the rubric hasn't loaded
except BrokenPromise:
return (False, None)
return False, None
# Successful if `feedback` is a non-empty list
else:
return (bool(feedback), feedback)
return bool(feedback), feedback
return _inner_check
......@@ -183,9 +183,11 @@ class SelfAssessmentTest(OpenResponseTest):
# Fill in the rubric and expect that we get feedback
rubric = self.open_response.rubric
self.assertEqual(rubric.categories, ["Writing Applications", "Language Conventions"])
rubric.set_scores([0, 1])
rubric.submit()
rubric.submit('self')
self.assertEqual(rubric.feedback, ['incorrect', 'correct'])
# Verify the progress page
......@@ -306,7 +308,7 @@ class PeerAssessmentTest(OpenResponseTest):
"""
Given I am viewing a peer-assessment problem
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.
Given I have submitted an essay for peer-assessment
......@@ -339,7 +341,7 @@ class PeerAssessmentTest(OpenResponseTest):
rubric = self.peer_calibrate.rubric
self.assertEqual(rubric.categories, ["Writing Applications", "Language Conventions"])
rubric.set_scores([0, 1])
rubric.submit()
rubric.submit('peer')
self.peer_calibrate.continue_to_grading()
# Grade a peer
......
"""
Acceptance tests for Studio.
"""
from unittest import expectedFailure
from unittest import skip
from bok_choy.web_app_test import WebAppTest
......@@ -25,7 +25,7 @@ from ..pages.studio.textbooks import TextbooksPage
from ..pages.xblock.acid import AcidView
from ..fixtures.course import CourseFixture, XBlockFixtureDesc
from .helpers import UniqueCourseTest
from .helpers import UniqueCourseTest, load_data_str
class LoggedOutTest(WebAppTest):
......@@ -237,8 +237,7 @@ class XBlockAcidParentBase(XBlockAcidBase):
acid_block = AcidView(self.browser, container.xblocks[0].preview_selector)
self.validate_acid_block_preview(acid_block)
# This will fail until the container page supports editing
@expectedFailure
@skip('This will fail until the container page supports editing')
def test_acid_block_editor(self):
super(XBlockAcidParentBase, self).test_acid_block_editor()
......@@ -299,12 +298,10 @@ class XBlockAcidChildTest(XBlockAcidParentBase):
)
).install()
# This will fail until we fix support of children in pure XBlocks
@expectedFailure
@skip('This will fail until we fix support of children in pure XBlocks')
def test_acid_block_preview(self):
super(XBlockAcidChildTest, self).test_acid_block_preview()
# This will fail until we fix support of children in pure XBlocks
@expectedFailure
@skip('This will fail until we fix support of children in pure XBlocks')
def test_acid_block_editor(self):
super(XBlockAcidChildTest, self).test_acid_block_editor()
......@@ -120,7 +120,7 @@ pep8==1.4.5
pylint==0.28
python-subunit==0.0.16
rednose==0.3
selenium==2.34.0
selenium==2.39.0
splinter==0.5.4
testtools==0.9.34
......
......@@ -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/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/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/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