"""An OpenStax Tutor Beta practice session."""
from __future__ import annotations
from time import sleep
from typing import List, Union
from pypom import Region
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webelement import WebElement
from pages.tutor.course import StudentCourse
from pages.tutor.performance import PerformanceForecast
from pages.tutor.task import Homework
from pages.web.errata import ErrataForm
from utils.tutor import TutorException
from utils.utilities import Utility, go_to_
# -------------------------------------------------------- #
# Javascript page requests
# -------------------------------------------------------- #
# get the modal and tooltip root that is a neighbor of the React root element
GET_ROOT = 'return document.querySelector("[role={0}]");'
# -------------------------------------------------------- #
# Tooltips and Dialog boxes
# -------------------------------------------------------- #
# -------------------------------------------------------- #
# Practice page
# -------------------------------------------------------- #
[docs]class Practice(Homework):
"""A practice session."""
_body_locator = (By.CSS_SELECTOR, 'body')
_exercise_breadcrumb_locator = (By.CSS_SELECTOR, '.breadcrumb-exercise')
_personalized_badge_locator = (By.CSS_SELECTOR, '.personalized')
_personalized_tooltip_locator = (By.CSS_SELECTOR, '.personalized svg')
_question_stimulus_locator = (By.CSS_SELECTOR, '.exercise-stimulus')
_question_stem_locator = (
By.CSS_SELECTOR, '.question-stem , [class*=QuestionStem]')
_free_response_box_locator = (By.CSS_SELECTOR, 'textarea')
_answer_button_locator = (By.CSS_SELECTOR, '.btn-primary')
_exercise_id_locator = (By.CSS_SELECTOR, '.exercise-identifier-link')
_suggest_a_correction_link_locator = (
By.CSS_SELECTOR, '.exercise-identifier-link a')
_book_section_locator = (By.CSS_SELECTOR, '.chapter-section')
_book_section_title_locator = (By.CSS_SELECTOR, '.title')
_view_book_section_link_locator = (By.CSS_SELECTOR, '.reference')
_debug_information_locator = (
By.CSS_SELECTOR, '.visible-when-debugging li')
_footer_root_locator = (
By.CSS_SELECTOR, '.tutor-navbar:not(:first-child)')
@property
def loaded(self) -> bool:
"""Return True when all loading messages are done.
:return: ``True`` if no loading message is found
:rtype: bool
"""
body_source = (self.find_element(*self._body_locator)
.get_attribute('outerHTML'))
loaded = ('Loading' not in body_source and
'is-loading' not in body_source)
if loaded:
sleep(1)
return loaded
@property
def exercises(self) -> int:
"""Return the number of practice assessments.
:return: the number of assessments in this practice session
:rtype: int
"""
return len(self.find_elements(*self._exercise_breadcrumb_locator))
@property
def is_personalized(self) -> bool:
"""Return True if the assessment is personalized to the user.
:return: ``True`` if the assessment is personalized, otherwise
``False``
:rtype: bool
"""
return bool(self.find_elements(*self._personalized_badge_locator))
@property
def personalized_tooltip(self) -> ButtonTooltip:
"""Hover over the personalized badge info icon to show the help text.
:return: the personalized assessment tooltip
:rtype: :py:class:`~pages.tutor.practice.ButtonTooltip`
"""
# TODO: hover over the icon and return the text content of the tooltip
@property
def stimulus(self) -> str:
"""Return the assessment stimulus if present.
:return: the assessment stimulus if present or an empty string if not
:rtype: str
"""
try:
return (self.find_element(*self._question_stimulus_locator)
.get_attribute('textContent'))
except NoSuchElementException:
return ''
@property
def stem(self) -> str:
"""Return the question stem content.
:return: the question stem text content
:rtype: str
"""
return (self.find_element(*self._question_stem_locator)
.get_attribute('textContent'))
@property
def has_free_response(self) -> bool:
"""Return True if the assessment has a free response text box.
:return: ``True`` if a free response textbox is present, else ``False``
:rtype: bool
"""
return bool(self.find_elements(*self._free_response_box_locator))
@property
def question(self) \
-> Union[Practice.MultipleChoice, Practice.FreeResponse]:
"""Access the step-type features of the assessment.
:return: the inner assessment function - either multiple choice or a
free response
:rtype: :py:class:`~pages.tutor.practice.Practice.MultipleChoice` or
:py:class:`~pages.tutor.practice.Practice.FreeResponse`
"""
if self.has_free_response:
return self.FreeResponse(self)
return self.MultipleChoice(self)
@property
def answer_enabled(self) -> bool:
"""Return True if the 'Answer' button is enabled.
:return: ``True`` if the Answer button is clickable, otherwise
``False``
:rtype: bool
"""
answer_button = self.find_element(*self._answer_button_locator)
return self.driver.execute_script('return !arguments[0].disabled;',
answer_button)
[docs] def answer(self) -> Practice:
"""Submit the answer.
:return: the next step in the practice session
:rtype: :py:class:`~pages.tutor.practice.Practice`
:raises :py:class:`~utils.tutor.TutorException`: if the answer button
is disabled
"""
if not self.answer_enabled:
raise TutorException("The answer button is currently disabled; "
"next step not available")
button = self.find_element(*self._answer_button_locator)
Utility.click_option(self.driver, element=button)
sleep(1)
return self
@property
def exercise_id(self) -> str:
"""Return the exercise identification number for the assessment.
:return: the Exercise ID number and version
:rtype: str
"""
return self.find_element(*self._exercise_id_locator).split()[1]
[docs] def suggest_a_correction(self) -> ErrataForm:
"""Click the 'Suggest a correction' link.
:return: the errata submission form for the course book
:rtype: :py:class:`~pages.web.errata.ErrataForm`
"""
link = self.find_element(*self._suggest_a_correction_link_locator)
Utility.switch_to(self.driver, element=link)
return go_to_(ErrataForm(self.driver))
@property
def section(self) -> str:
"""Return the assessment's associated chapter section.
:return: the chapter and section containing the answer to the current
assessment
:rtype: str
"""
return self.find_element(*self._book_section_locator).text
@property
def section_title(self) -> str:
"""Return the assessment's associated chapter section title.
:return: the title for the book section containing the answer to the
current assessment
:rtype: str
"""
return self.find_element(*self._book_section_title_locator).text
@property
def footer(self) -> Practice.Footer:
"""Access the practice assignment footer.
:return: the footer region
:rtype: :py:class:`~pages.tutor.practice.Practice.Footer`
"""
footer_root = self.find_element(*self._footer_root_locator)
return self.Footer(self, footer_root)
[docs] class FreeResponse(Region):
"""A free response assessment step."""
_nudge_shown_locator = (By.CSS_SELECTOR, '[class*=NudgeMessage]')
_submit_this_answer_locator = (
By.CSS_SELECTOR, '.related-content-link ~ a')
@property
def free_response(self) -> str:
"""Return the current content of the free response text box.
:return: the free response content
:rtype: str
"""
if not self.has_free_response:
return ''
return (self.find_element(*self.page._free_response_box_locator)
.get_attribute('textContent'))
@free_response.setter
def free_response(self, answer: str) -> None:
"""Send the answer to the free response text box.
:param str answer: the answer text to send to the free response
text box
:return: None
"""
if not self.has_free_response:
return
self.find_element(*self._free_response_box_locator) \
.send_keys(answer)
@property
def nudge_shown(self) -> bool:
"""Return True if the answer validation message is displayed.
If the nudge message is displayed, then the free response failed
answer validation.
:return: ``True`` if the validation message is displayed, otherwise
``False``
"""
return bool(self.find_elements(*self._nudge_shown_locator))
[docs] def submit_this_answer(self) -> Practice:
"""Submit the invalid free response.
:return: the multiple choice answer step for the assessment
:rtype: :py:class:`~pages.tutor.practice.Practice`
"""
link = self.find_element(*self._submit_this_answer_locator)
Utility.click_option(self.driver, element=link)
sleep(1)
return self
[docs] class MultipleChoice(Region):
"""A multiple choice assessment step."""
_instructions_locator = (By.CSS_SELECTOR, '.instructions')
_answer_option_locator = (By.CSS_SELECTOR, '.openstax-answer')
@property
def instructions(self) -> str:
"""Return the assessment instructions.
:return: the assessment instructions
:rtype: str
"""
return self.find_element(*self._instructions_locator).text
@property
def answers(self) -> List[Practice.MultipleChoice.Answer]:
r"""Access the multiple choice answers.
:return: the list of available answer options
:rtype: list(:py:class:`~pages.tutor.practice. \
Practice.MultipleChoice.Answer`)
"""
return [self.Answer(self, option)
for option
in self.find_elements(*self._answer_option_locator)]
[docs] class Answer(Region):
"""An assessment answer choice."""
_question_id_locator = (By.CSS_SELECTOR, '[type=radio]')
_answer_letter_locator = (By.CSS_SELECTOR, 'button')
_answer_content_locator = (By.CSS_SELECTOR, '.answer-content')
_is_selected_status_locator = (By.CSS_SELECTOR, 'section')
@property
def question_id(self) -> int:
"""Return the assessment question identification number.
:return: the question ID number
:rtype: int
"""
return int(self.find_element(*self._question_id_locator)
.get_attribute('id')
.split('-')[0])
@property
def letter(self) -> str:
"""Return the answer option letter.
:return: the answer letter
:rtype: str
"""
return self.find_element(*self._answer_letter_locator).text
@property
def answer(self) -> str:
"""Return the answer content.
:return: the answer content text
:rtype: str
"""
return (self.find_element(*self._answer_content_locator)
.get_attribute('textContent'))
[docs] def select(self) -> None:
"""Click on the answer to select it.
:return: None
"""
button = self.find_element(*self._answer_letter_locator)
Utility.click_option(self.driver, element=button)
sleep(0.5)
@property
def is_selected(self) -> bool:
"""Return True if the answer option is currently selected.
:return: ``True`` if the answer is selected, otherwise
``False``
:rtype: bool
"""
status = self.find_element(*self._is_selected_status_locator)
return 'answer-checked' in status.get_attribute('class')
[docs] class Nav(Region):
"""The practice session assessment navigation."""
_root_locator = (By.CSS_SELECTOR, '[class*=SecondaryToolbar]')
_step_icon_locator = (By.CSS_SELECTOR, '.breadcrumb-exercise')
_completion_locator = (By.CSS_SELECTOR, '.breadcrumb-end')
@property
def steps(self) -> List[Practice.Nav.Icon]:
"""Access the practice session steps.
:return: the list of available assessment steps
:rtype: list(:py:class:`~Practice.Nav.Icon`)
"""
return [self.Icon(self, step)
for step
in self.find_elements(*self._step_icon_locator)]
@property
def completion(self) -> Practice.Nav.Icon:
"""Access the completion/final step.
:return: the completion step
:rtype: :py:class:`~Practice.Nav.Icon`
"""
return self.Icon(self,
self.find_element(*self._completion_locator))
[docs] class Icon(Region):
"""A practice session step icon."""
@property
def is_actice(self) -> bool:
"""Return True if this step is currently active.
:return: ``True`` if the step is active and displayed,
otherwise ``False``
:rtype: bool
"""
return 'active' in self.root.get_attribute('class')
@property
def position(self) -> int:
"""Return the step's position.
:return: the step's positon within the practice session
:rtype: int
"""
return int(self.root.get_attribute('data-step-index'))
@property
def step_id(self) -> int:
"""Return the step identification number.
:return: the step ID number
:rtype: int
"""
return int(self.root.get_attribute('data-step-id'))
[docs] def select(self) -> Practice:
"""Click the icon to view the practice step.
:return: the practice session with the selected step in the
practice window
:rtype: :py:class:`~pages.tutor.practice.Practice`
"""
Utility.click_option(self.driver, element=self.root)
sleep(1)
return self.page.page