This will remove imports from __future__ that are no longer needed. https://docs.python.org/3.5/library/2to3.html#2to3fixer-future
642 lines
23 KiB
Python
642 lines
23 KiB
Python
"""
|
|
Problem Page.
|
|
"""
|
|
|
|
|
|
from bok_choy.page_object import PageObject
|
|
from selenium.webdriver import ActionChains
|
|
from selenium.webdriver.common.keys import Keys
|
|
|
|
from common.test.acceptance.pages.common.utils import click_css
|
|
|
|
|
|
class ProblemPage(PageObject):
|
|
"""
|
|
View of problem page.
|
|
"""
|
|
|
|
url = None
|
|
CSS_PROBLEM_HEADER = '.problem-header'
|
|
status_indicators = {
|
|
'correct': ['span.correct'],
|
|
'incorrect': ['span.incorrect'],
|
|
'unanswered': ['span.unanswered'],
|
|
'submitted': ['span.submitted'],
|
|
'unsubmitted': ['.unsubmitted']
|
|
}
|
|
|
|
def is_browser_on_page(self):
|
|
return self.q(css='.xblock-student_view').present
|
|
|
|
@property
|
|
def problem_name(self):
|
|
"""
|
|
Return the current problem name.
|
|
"""
|
|
self.wait_for_element_visibility(self.CSS_PROBLEM_HEADER, 'wait for problem header')
|
|
return self.q(css='.problem-header').text[0]
|
|
|
|
@property
|
|
def problem_text(self):
|
|
"""
|
|
Return the text of the question of the problem.
|
|
"""
|
|
return self.q(css="div.problem p").text
|
|
|
|
@property
|
|
def problem_input_content(self):
|
|
"""
|
|
Return the text of the question of the problem.
|
|
"""
|
|
return self.q(css="div.wrapper-problem-response").text[0]
|
|
|
|
@property
|
|
def problem_content(self):
|
|
"""
|
|
Return the content of the problem
|
|
"""
|
|
return self.q(css="div.problems-wrapper").text[0]
|
|
|
|
@property
|
|
def problem_meta(self):
|
|
"""
|
|
Return the problem meta text
|
|
"""
|
|
return self.q(css=".problems-wrapper .problem-progress").text[0]
|
|
|
|
@property
|
|
def message_text(self):
|
|
"""
|
|
Return the "message" text of the question of the problem.
|
|
"""
|
|
return self.q(css="div.problem span.message").text[0]
|
|
|
|
@property
|
|
def extract_hint_text_from_html(self):
|
|
"""
|
|
Return the "hint" text of the problem from html
|
|
"""
|
|
hints_html = self.q(css="div.problem .notification-hint .notification-message li").html
|
|
return [hint_html.split(' <span', 1)[0] for hint_html in hints_html]
|
|
|
|
@property
|
|
def hint_text(self):
|
|
"""
|
|
Return the "hint" text of the problem from its div.
|
|
"""
|
|
return self.q(css="div.problem .notification-hint .notification-message").text[0]
|
|
|
|
def verify_mathjax_rendered_in_problem(self):
|
|
"""
|
|
Check that MathJax have been rendered in problem hint
|
|
"""
|
|
def mathjax_present():
|
|
""" Returns True if MathJax css is present in the problem body """
|
|
mathjax_container = self.q(css="div.problem p .MathJax")
|
|
return mathjax_container.visible and mathjax_container.present
|
|
|
|
self.wait_for(
|
|
mathjax_present,
|
|
description="MathJax rendered in problem body"
|
|
)
|
|
|
|
def verify_mathjax_rendered_in_preview(self):
|
|
"""
|
|
Check that MathJax has been rendered in formula problem preview
|
|
"""
|
|
|
|
def mathjax_present():
|
|
""" Returns True if MathJax css is present inside the preview """
|
|
mathjax_container = self.q(css="div.problem div .MathJax")
|
|
return mathjax_container.visible and mathjax_container.present
|
|
|
|
self.wait_for(
|
|
mathjax_present,
|
|
description="MathJax rendered in problem preview"
|
|
)
|
|
|
|
def verify_mathjax_rendered_in_hint(self):
|
|
"""
|
|
Check that MathJax have been rendered in problem hint
|
|
"""
|
|
def mathjax_present():
|
|
""" Returns True if MathJax css is present in the problem body """
|
|
mathjax_container = self.q(css="div.problem div.problem-hint .MathJax")
|
|
return mathjax_container.visible and mathjax_container.present
|
|
|
|
self.wait_for(
|
|
mathjax_present,
|
|
description="MathJax rendered in hint"
|
|
)
|
|
|
|
def fill_answer(self, text, input_num=None):
|
|
"""
|
|
Fill in the answer to the problem.
|
|
|
|
args:
|
|
text: String to fill the input with.
|
|
|
|
kwargs:
|
|
input_num: If provided, fills only the input_numth field. Else, all
|
|
input fields will be filled.
|
|
"""
|
|
fields = self.q(css='div.problem div.capa_inputtype.textline input')
|
|
fields = fields.nth(input_num) if input_num is not None else fields
|
|
fields.fill(text)
|
|
|
|
def fill_answer_numerical(self, text):
|
|
"""
|
|
Fill in the answer to a numerical problem.
|
|
"""
|
|
self.q(css='div.problem div.inputtype input').fill(text)
|
|
self.wait_for_element_invisibility('.loading', 'wait for loading icon to disappear')
|
|
self.wait_for_ajax()
|
|
|
|
@property
|
|
def get_numerical_input_value(self):
|
|
"""
|
|
Get the numerical problem input contents
|
|
"""
|
|
return self.q(css='div.problem div.inputtype input').text[0]
|
|
|
|
def click_submit(self):
|
|
"""
|
|
Click the Submit button.
|
|
"""
|
|
click_css(self, '.problem .submit', require_notification=False)
|
|
|
|
def click_save(self):
|
|
"""
|
|
Click the Save button.
|
|
"""
|
|
click_css(self, '.problem .save', require_notification=False)
|
|
|
|
def click_reset(self):
|
|
"""
|
|
Click the Reset button.
|
|
"""
|
|
click_css(self, '.problem .reset', require_notification=False)
|
|
self.wait_for_ajax()
|
|
|
|
def click_show(self):
|
|
"""
|
|
Click the Show Answer button.
|
|
"""
|
|
css = '.problem .show'
|
|
# First make sure that the button visible and can be clicked on.
|
|
self.scroll_to_element(css)
|
|
self.q(css=css).click()
|
|
self.wait_for_ajax()
|
|
|
|
def is_hint_notification_visible(self):
|
|
"""
|
|
Is the Hint Notification visible?
|
|
"""
|
|
return self.q(css='.notification.notification-hint').visible
|
|
|
|
def is_feedback_message_notification_visible(self):
|
|
"""
|
|
Is the Feedback Messaged notification visible
|
|
"""
|
|
return self.q(css='.wrapper-problem-response .message').visible
|
|
|
|
def is_save_notification_visible(self):
|
|
"""
|
|
Is the Save Notification Visible?
|
|
"""
|
|
return self.q(css='.notification.warning.notification-save').visible
|
|
|
|
def is_success_notification_visible(self):
|
|
"""
|
|
Is the Submit Notification Visible?
|
|
"""
|
|
return self.q(css='.notification.success.notification-submit').visible
|
|
|
|
def wait_for_feedback_message_visibility(self):
|
|
"""
|
|
Wait for the Feedback Message notification to be visible.
|
|
"""
|
|
self.wait_for_element_visibility('.wrapper-problem-response .message',
|
|
'Waiting for the Feedback message to be visible')
|
|
|
|
def wait_for_save_notification(self):
|
|
"""
|
|
Wait for the Save Notification to be present
|
|
"""
|
|
self.wait_for_element_visibility('.notification.warning.notification-save',
|
|
'Waiting for Save notification to be visible')
|
|
self.wait_for(lambda: self.q(css='.notification.warning.notification-save').focused,
|
|
'Waiting for the focus to be on the save notification')
|
|
|
|
def wait_for_gentle_alert_notification(self):
|
|
"""
|
|
Wait for the Gentle Alert Notification to be present
|
|
"""
|
|
self.wait_for_element_visibility('.notification.warning.notification-gentle-alert',
|
|
'Waiting for Gentle Alert notification to be visible')
|
|
self.wait_for(lambda: self.q(css='.notification.warning.notification-gentle-alert').focused,
|
|
'Waiting for the focus to be on the gentle alert notification')
|
|
|
|
def wait_for_show_answer_notification(self):
|
|
"""
|
|
Wait for the show answer Notification to be present
|
|
"""
|
|
self.wait_for_element_visibility('.notification.general.notification-show-answer',
|
|
'Waiting for Show Answer notification to be visible')
|
|
self.wait_for(lambda: self.q(css='.notification.general.notification-show-answer').focused,
|
|
'Waiting for the focus to be on the show answer notification')
|
|
|
|
def is_gentle_alert_notification_visible(self):
|
|
"""
|
|
Is the Gentle Alert Notification visible?
|
|
"""
|
|
return self.q(css='.notification.warning.notification-gentle-alert').visible
|
|
|
|
def is_reset_button_present(self):
|
|
""" Check for the presence of the reset button. """
|
|
return self.q(css='.problem .reset').present
|
|
|
|
def is_save_button_enabled(self):
|
|
""" Is the Save button enabled """
|
|
return self.q(css='.action .save').attrs('disabled') == [None]
|
|
|
|
def is_focus_on_problem_meta(self):
|
|
"""
|
|
Check for focus problem meta.
|
|
"""
|
|
return self.q(css='.problem-header').focused
|
|
|
|
def wait_for_focus_on_problem_meta(self):
|
|
"""
|
|
Waits for focus on Problem Meta section
|
|
"""
|
|
self.wait_for(
|
|
promise_check_func=self.is_focus_on_problem_meta,
|
|
description='Waiting for focus on Problem Meta section'
|
|
)
|
|
|
|
def is_submit_disabled(self):
|
|
"""
|
|
Checks if the submit button is disabled
|
|
"""
|
|
disabled_attr = self.q(css='.problem .submit').attrs('disabled')[0]
|
|
return disabled_attr == 'true'
|
|
|
|
def wait_for_submit_disabled(self):
|
|
"""
|
|
Waits until the Submit button becomes disabled.
|
|
"""
|
|
self.wait_for(self.is_submit_disabled, 'Waiting for submit to be enabled')
|
|
|
|
def wait_for_focus_on_submit_notification(self):
|
|
"""
|
|
Check for focus submit notification.
|
|
"""
|
|
|
|
def focus_check():
|
|
"""
|
|
Checks whether or not the focus is on the notification-submit
|
|
"""
|
|
return self.q(css='.notification-submit').focused
|
|
|
|
self.wait_for(promise_check_func=focus_check, description='Waiting for the notification-submit to gain focus')
|
|
|
|
def wait_for_status_icon(self):
|
|
"""
|
|
wait for status icon
|
|
"""
|
|
self.wait_for_element_visibility('div.problem div.inputtype div .status', 'wait for status icon')
|
|
|
|
def wait_for_expected_status(self, status_selector, message):
|
|
"""
|
|
Waits for the expected status indicator.
|
|
|
|
Args:
|
|
status_selector(str): status selector string.
|
|
message(str): description of promise, to be logged.
|
|
"""
|
|
msg = u"Wait for status to be {}".format(message)
|
|
self.wait_for_element_visibility(status_selector, msg)
|
|
|
|
def is_expected_status_visible(self, status_selector):
|
|
"""
|
|
check for the expected status indicator to be visible.
|
|
|
|
Args:
|
|
status_selector(str): status selector string.
|
|
"""
|
|
return self.q(css=status_selector).visible
|
|
|
|
def wait_success_notification(self):
|
|
"""
|
|
Check for visibility of the success notification and icon.
|
|
"""
|
|
msg = "Wait for success notification to be visible"
|
|
self.wait_for_element_visibility('.notification.success.notification-submit', msg)
|
|
self.wait_for_element_visibility('.fa-check', "Waiting for success icon")
|
|
self.wait_for_focus_on_submit_notification()
|
|
|
|
def wait_incorrect_notification(self):
|
|
"""
|
|
Check for visibility of the incorrect notification and icon.
|
|
"""
|
|
msg = "Wait for error notification to be visible"
|
|
self.wait_for_element_visibility('.notification.error.notification-submit', msg)
|
|
self.wait_for_element_visibility('.fa-close', "Waiting for incorrect notification icon")
|
|
self.wait_for_focus_on_submit_notification()
|
|
|
|
def wait_partial_notification(self):
|
|
"""
|
|
Check for visibility of the partially visible notification and icon.
|
|
"""
|
|
msg = "Wait for partial correct notification to be visible"
|
|
self.wait_for_element_visibility('.notification.success.notification-submit', msg)
|
|
self.wait_for_element_visibility('.fa-asterisk', "Waiting for asterisk notification icon")
|
|
self.wait_for_focus_on_submit_notification()
|
|
|
|
def wait_submitted_notification(self):
|
|
"""
|
|
Check for visibility of the "answer received" general notification and icon.
|
|
"""
|
|
msg = "Wait for submitted notification to be visible"
|
|
self.wait_for_element_visibility('.notification.general.notification-submit', msg)
|
|
self.wait_for_focus_on_submit_notification()
|
|
|
|
def click_hint(self, hint_index=0):
|
|
"""
|
|
Click the Hint button.
|
|
|
|
Arguments:
|
|
hint_index (int): Index of a displayed hint
|
|
"""
|
|
click_css(self, '.problem .hint-button', require_notification=False)
|
|
self.wait_for_focus_on_hint_notification(hint_index)
|
|
|
|
def wait_for_focus_on_hint_notification(self, hint_index=0):
|
|
"""
|
|
Wait for focus to be on the hint notification.
|
|
|
|
Arguments:
|
|
hint_index (int): Index of a displayed hint
|
|
"""
|
|
css = u'.notification-hint .notification-message > ol > li.hint-index-{hint_index}'.format(
|
|
hint_index=hint_index
|
|
)
|
|
self.wait_for(
|
|
lambda: self.q(css=css).focused,
|
|
'Waiting for the focus to be on the hint notification'
|
|
)
|
|
|
|
def click_review_in_notification(self, notification_type):
|
|
"""
|
|
Click on the "Review" button within the visible notification.
|
|
"""
|
|
css_string = u'.notification.notification-{notification_type} .review-btn'.format(
|
|
notification_type=notification_type
|
|
)
|
|
|
|
# The review button cannot be clicked on until it is tabbed to, so first tab to it.
|
|
# Multiple tabs may be required depending on the content (for instance, hints with links).
|
|
def tab_until_review_focused():
|
|
""" Tab until the review button is focused """
|
|
self.browser.switch_to_active_element().send_keys(Keys.TAB)
|
|
if self.q(css=css_string).focused:
|
|
self.scroll_to_element(css_string)
|
|
return self.q(css=css_string).focused
|
|
|
|
self.wait_for(
|
|
tab_until_review_focused,
|
|
'Waiting for the Review button to become focused'
|
|
)
|
|
self.wait_for_element_visibility(
|
|
css_string,
|
|
'Waiting for the button to be visible'
|
|
)
|
|
click_css(self, css_string, require_notification=False)
|
|
|
|
def get_hint_button_disabled_attr(self):
|
|
""" Return the disabled attribute of all hint buttons (once hints are visible, there will be two). """
|
|
return self.q(css='.problem .hint-button').attrs('disabled')
|
|
|
|
def click_choice(self, choice_value):
|
|
"""
|
|
Click the choice input(radio, checkbox or option) where value matches `choice_value` in choice group.
|
|
"""
|
|
self.q(css='div.problem .choicegroup input[value="' + choice_value + '"]').first.click()
|
|
self.wait_for_ajax()
|
|
|
|
def is_correct(self):
|
|
"""
|
|
Is there a "correct" status showing?
|
|
"""
|
|
return self.q(css="div.problem div.capa_inputtype.textline div.correct span.status").is_present()
|
|
|
|
def simpleprob_is_correct(self):
|
|
"""
|
|
Is there a "correct" status showing? Works with simple problem types.
|
|
"""
|
|
return self.q(css="div.problem div.inputtype div.correct span.status").is_present()
|
|
|
|
def simpleprob_is_partially_correct(self):
|
|
"""
|
|
Is there a "partially correct" status showing? Works with simple problem types.
|
|
"""
|
|
return self.q(css="div.problem div.inputtype div.partially-correct span.status").is_present()
|
|
|
|
def simpleprob_is_incorrect(self):
|
|
"""
|
|
Is there an "incorrect" status showing? Works with simple problem types.
|
|
"""
|
|
return self.q(css="div.problem div.inputtype div.incorrect span.status").is_present()
|
|
|
|
def get_simpleprob_correctness(self):
|
|
"""
|
|
Returns the correctness status for a simple problem.
|
|
|
|
Given a simple problem, the method returns the correctness status.
|
|
If there is no visible status, None is returned
|
|
"""
|
|
|
|
if self.simpleprob_is_correct():
|
|
return 'correct'
|
|
elif self.simpleprob_is_incorrect():
|
|
return 'incorrect'
|
|
elif self.simpleprob_is_partially_correct():
|
|
return 'partial'
|
|
else:
|
|
return None
|
|
|
|
def click_clarification(self, index=0):
|
|
"""
|
|
Click on an inline icon that can be included in problem text using an HTML <clarification> element:
|
|
|
|
Problem <clarification>clarification text hidden by an icon in rendering</clarification> Text
|
|
"""
|
|
self.q(css=u'div.problem .clarification:nth-child({index}) span[data-tooltip]'.format(index=index + 1)).click()
|
|
|
|
@property
|
|
def visible_tooltip_text(self):
|
|
"""
|
|
Get the text seen in any tooltip currently visible on the page.
|
|
"""
|
|
self.wait_for_element_visibility('body > .tooltip', 'A tooltip is visible.')
|
|
return self.q(css='body > .tooltip').text[0]
|
|
|
|
def is_solution_tag_present(self):
|
|
"""
|
|
Check if solution/explanation is shown.
|
|
"""
|
|
solution_selector = '.solution-span div.detailed-solution'
|
|
return self.q(css=solution_selector).is_present()
|
|
|
|
def is_choice_highlighted(self, choice, choices_list, show_answer=True):
|
|
"""
|
|
Check if the given answer/choice is highlighted for choice group.
|
|
|
|
show_answer: if set, then requires each choice to be marked with a status.
|
|
If not set, then the status can be elswhere in the problem.
|
|
"""
|
|
if show_answer:
|
|
choice_status_xpath = (u'//fieldset/div[contains(@class, "field")][{{0}}]'
|
|
u'/label[contains(@class, "choicegroup_{choice}")]'
|
|
u'/span[contains(@class, "status {choice}")]'.format(choice=choice))
|
|
any_status_xpath = u'//fieldset/div[contains(@class, "field")][{0}]/label/span'
|
|
else:
|
|
choice_status_xpath = (u'//fieldset/div[contains(@class, "field")][{{0}}]'
|
|
u'/label[contains(@class, "choicegroup_{choice}")]'.format(choice=choice))
|
|
any_status_xpath = u'//div[contains(@class, "indicator-container")]/span[contains(@class, "status")]'
|
|
|
|
for possible_choice in choices_list:
|
|
if not self.q(xpath=choice_status_xpath.format(possible_choice)).is_present():
|
|
return False
|
|
|
|
# Check that there is only a single status span, as there were some bugs with multiple
|
|
# spans (with various classes) being appended.
|
|
if not len(self.q(xpath=any_status_xpath.format(possible_choice)).results) == 1:
|
|
return False
|
|
|
|
return True
|
|
|
|
def is_correct_choice_highlighted(self, correct_choices, show_answer=True):
|
|
"""
|
|
Check if correct answer/choice highlighted for choice group.
|
|
"""
|
|
return self.is_choice_highlighted('correct', correct_choices, show_answer)
|
|
|
|
def is_submitted_choice_highlighted(self, correct_choices):
|
|
"""
|
|
Check if submitted answer/choice highlighted for choice group.
|
|
"""
|
|
return self.is_choice_highlighted('submitted', correct_choices)
|
|
|
|
@property
|
|
def problem_question(self):
|
|
"""
|
|
Return the question text of the problem.
|
|
"""
|
|
return self.q(css="div.problem .wrapper-problem-response legend").text[0]
|
|
|
|
@property
|
|
def problem_question_descriptions(self):
|
|
"""
|
|
Return a list of question descriptions of the problem.
|
|
"""
|
|
return self.q(css="div.problem .wrapper-problem-response .question-description").text
|
|
|
|
@property
|
|
def problem_progress_graded_value(self):
|
|
"""
|
|
Return problem progress text which contains weight of problem, if it is graded, and the student's current score.
|
|
"""
|
|
self.wait_for_element_visibility('.problem-progress', "Problem progress is visible")
|
|
return self.q(css='.problem-progress').text[0]
|
|
|
|
@property
|
|
def status_sr_text(self):
|
|
"""
|
|
Returns the text in the special "sr" region used for display status.
|
|
"""
|
|
return self.q(css='#reader-feedback').text[0]
|
|
|
|
@property
|
|
def submission_feedback(self):
|
|
"""
|
|
Returns the submission feedback of the problem
|
|
"""
|
|
return self.q(css='div[class="submission-feedback"]').text[0].split('\n')[0]
|
|
|
|
@property
|
|
def answer(self):
|
|
"""
|
|
Returns the answer of the problem
|
|
"""
|
|
return self.q(css='p[class="answer"]').text[0]
|
|
|
|
@property
|
|
def score_notification(self):
|
|
"""
|
|
Returns the score after the submission of answer
|
|
"""
|
|
self.wait_for_element_visibility('.notification-submit .notification-message', 'Problem score is visible')
|
|
return self.q(css='.notification-submit .notification-message').text[0]
|
|
|
|
def is_present(self, selector):
|
|
"""
|
|
Checks for the presence of the locator
|
|
"""
|
|
return self.q(css=selector).present
|
|
|
|
|
|
class DragAndDropPage(PageObject):
|
|
"""
|
|
View for a Drag & Drop problem.
|
|
"""
|
|
|
|
url = None
|
|
|
|
def is_browser_on_page(self):
|
|
return self.q(css='.xblock-student_view').present
|
|
|
|
def is_submit_disabled(self):
|
|
"""
|
|
Checks if the submit button is disabled for Drag & Drop problem.
|
|
"""
|
|
disabled_attr = self.q(css='.submit-answer-button').attrs('disabled')[0]
|
|
return disabled_attr == 'true'
|
|
|
|
def is_present(self, selector):
|
|
"""
|
|
Checks for the presence of the locator.
|
|
"""
|
|
return self.q(css=selector).present
|
|
|
|
def is_submit_button_present(self):
|
|
"""
|
|
Verifies if the submit button is present for DnD problems
|
|
with assessment mode.
|
|
"""
|
|
return self.is_present('.submit-answer-button')
|
|
|
|
def _get_item_by_value(self, item_value):
|
|
"""
|
|
Get the item that will be placed onto a zone.
|
|
"""
|
|
return self.q(xpath=(".//div[@data-value='{item_id}']".format(item_id=item_value)))[0]
|
|
|
|
def _get_zone_by_id(self, zone_id):
|
|
"""
|
|
Get zone where the item will be placed.
|
|
"""
|
|
zones_container = self.browser.find_element_by_css_selector('.target')
|
|
return zones_container.find_elements_by_xpath(".//div[@data-uid='{zone_id}']".format(zone_id=zone_id))[0]
|
|
|
|
def drag_item_to_zone(self, item_value, zone_id):
|
|
"""
|
|
Drag item to desired zone using mouse interaction.
|
|
"""
|
|
element = self._get_item_by_value(item_value)
|
|
target = self._get_zone_by_id(zone_id)
|
|
action_chains = ActionChains(self.browser)
|
|
action_chains.drag_and_drop(element, target).perform()
|
|
self.wait_for_ajax()
|