"""Student enrollment."""
from __future__ import annotations
from time import sleep
from typing import List, Tuple, Union
from pypom import Page, Region
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as expect
from pages.accounts.signup import Signup as AccountSignup
from pages.tutor.base import TutorBase
from pages.tutor.course import StudentCourse
from utils.tutor import Tutor, TutorException
from utils.utilities import Utility, go_to_
# get the modal and tooltip root that is a neighbor of the React root element
GET_ROOT = 'return document.querySelector("[role={0}]");'
# A By-styled selector
Selector = Tuple[str, str]
# -------------------------------------------------------- #
# Page dialog boxes
# -------------------------------------------------------- #
[docs]class Modal(Region):
"""A page modal."""
@property
def root(self) -> WebElement:
"""Return the root element for a page modal.
:return: the root element for a page modal
:rtype: :py:class:`~selenium.webdriver.remote.webelement.WebElement`
"""
return self.driver.execute_script(GET_ROOT.format('dialog'))
[docs]class BuyAccess(Modal):
"""The product purchase modal."""
_buy_access_now_button_locator = (By.CSS_SELECTOR, '.now')
_try_free_button_locator = (By.CSS_SELECTOR, '.later')
[docs] def buy_access_now(self) -> PurchaseForm:
"""Click the 'Buy access now' button.
:return: the purchase form modal
:rtype: :py:class:`~pages.tutor.enrollment.PurchaseForm`
"""
button = self.find_element(*self._buy_access_now_button_locator)
Utility.click_option(self.driver, element=button)
sleep(1)
return PurchaseForm(self.page)
[docs] def try_free(self) -> FreeTrial:
"""Click the 'Try free' button.
:return: the free trial modal
:rtype: :py:class:`~pages.tutor.enrollment.FreeTrial`
"""
button = self.find_element(*self._try_free_button_locator)
Utility.click_option(self.driver, element=button)
sleep(1)
return FreeTrial(self.page)
[docs]class FreeTrial(Modal):
"""The free product trial notice modal."""
_modal_content_locator = (By.CSS_SELECTOR, '.body')
_access_your_course_button_locator = (By.CSS_SELECTOR, '.now')
@property
def content(self) -> str:
"""Return the modal content text.
:return: the modal content
:rtype: str
"""
return (self.find_element(*self._modal_content_locator)
.get_attribute('textContent'))
[docs] def access_your_course(self) -> StudentCourse:
"""Click the 'Access your course' button.
:return: the student course page
:rtype: :py:class:`~pages.tutor.course.StudentCourse`
"""
button = self.find_element(*self._access_your_course_button_locator)
Utility.click_option(self.driver, element=button)
return go_to_(StudentCourse(self.driver, base_url=self.page.base_url))
[docs]class PrivacyPolicy(Modal):
"""The privacy policy enrollment modal."""
_modal_heading_locator = (By.CSS_SELECTOR, '.modal-header')
_modal_title_locator = (By.CSS_SELECTOR, '.title')
_modal_content_locator = (By.CSS_SELECTOR, '.title ~ div')
_i_agree_button_locator = (By.CSS_SELECTOR, '.btn-primary')
@property
def loaded(self) -> bool:
"""Return True when 'Terms of Service' is found on the page.
:return: ``True`` when the terms of service is found in the page text
:rtype: bool
"""
return ('Terms of Service'
in (self.find_element(*self._modal_content_locator)
.get_attribute('textContent')))
@property
def heading(self) -> str:
"""Return the modal heading.
:return: the privacy policy modal heading
:rtype: str
"""
return (self.find_element(*self._modal_heading_locator)
.get_attribute('textContent'))
@property
def title(self) -> str:
"""Return the modal title.
:return: the privacy policy modal title
:rtype: str
"""
return self.find_element(*self._modal_title_locator).text
@property
def content(self) -> str:
"""Return the modal body text.
:return: the modal body text
:rtype: str
"""
return (self.find_element(*self._modal_content_locator)
.get_attribute('textContent'))
[docs] def i_agree(self) -> Union[BuyAccess, StudentCourse]:
"""Click on the 'I agree' button.
After clicking on the I agree button, one of two destinations are
possible:
1. the student course page with the product purchase modal open for
paid courses
#. the student course page without a modal open for existing
students in a free course
:return: the course page with the product purchase modal displayed
:rtype: :py:class:`~pages.tutor.enrollment.BuyAccess` or
:py:class:`~pages.tutor.course.StudentCourse`
"""
button = self.find_element(*self._i_agree_button_locator)
Utility.click_option(self.driver, element=button)
sleep(1.25)
course = StudentCourse(self.driver, base_url=self.page.base_url)
dialog_root = self.driver.execute_script(GET_ROOT.format('dialog'))
if (dialog_root and
'pay-now-or-later' in dialog_root.get_attribute('class')):
return BuyAccess(course, dialog_root)
return go_to_(course)
[docs]class IframeModal(Modal):
"""A dialog box with internal iFrames."""
_base_iframe_locator = (By.CSS_SELECTOR, 'iframe')
def _get_value(self, locator: Selector, field: str = 'value',
inner_frame: Selector = None) -> str:
"""Return a purchase form value.
:param locator: a By-styled element selector for the requested element
field
:type locator: (str, str)
:param str field: (optional) the element field to read, default is to
return the input ``value`` field
:param inner_frame: (optional) a By-styled element selector for the
inner (second-order) iframe
:return: a form input's current value
:rtype: str
:noindex:
"""
purchase = self.find_element(*self._base_iframe_locator)
self.driver.switch_to.frame(purchase)
if inner_frame:
second_frame = self.find_element(*inner_frame)
self.driver.switch_to.frame(second_frame)
value = self.find_element(*locator).get_attribute(field)
if inner_frame:
self.driver.switch_to.default_content()
self.driver.switch_to.default_content()
return value
def _set_value(self, locator: Selector, value: str,
inner_frame: Selector = None) -> None:
"""Assign a value to a purchase form field.
:param locator: a By-styled element selector for the requested element
field
:type locator: (str, str)
:param str value: the value to assign
:param inner_frame: (optional) a By-styled element selector for the
inner (second-order) iframe
:return: None
:noindex:
"""
purchase = self.find_element(*self._base_iframe_locator)
self.driver.switch_to.frame(purchase)
if inner_frame:
second_frame = self.wait.until(
lambda _: self.find_element(*inner_frame))
self.driver.switch_to.frame(second_frame)
self.find_element(*locator).send_keys(value)
if inner_frame:
self.driver.switch_to.default_content()
self.driver.switch_to.default_content()
[docs]class PurchaseConfirmation(IframeModal):
"""The Tutor product purchase confirmation."""
_content_locator = (By.CSS_SELECTOR, 'h3 , p')
_order_date_locator = (By.CSS_SELECTOR, '.date span:last-child')
_order_number_locator = (By.CSS_SELECTOR, '.number span:last-child')
_product_name_locator = (By.CSS_SELECTOR, '.price span:first-child')
_product_price_locator = (By.CSS_SELECTOR, '.price span:last-child')
_tax_type_locator = (By.CSS_SELECTOR, '.tax span:first-child')
_tax_total_locator = (By.CSS_SELECTOR, '.tax span:last-child')
_sales_total_locator = (By.CSS_SELECTOR, '.total span:last-child')
_access_your_course_button_locator = (By.CSS_SELECTOR, 'button')
@property
def loaded(self) -> bool:
"""Return True when the content is present in the iframe.
:return: ``True`` when the content in the payment confirmation iframe
is present
:rtype: bool
"""
if Utility.is_browser(self.driver, 'chrome'):
return bool(self.order_number) and bool(self.total)
else:
return sleep(3.0) or True
@property
def content(self) -> str:
"""Return the order completion text.
:return: the text content at the top of the order confirmation pane
:rtype: str
"""
confirmation = self.find_element(*self._base_iframe_locator)
self.driver.switch_to.frame(confirmation)
text = '\n'.join(list([
line.text
for line
in self.find_elements(*self._content_locator)]))
self.driver.switch_to.default_content()
return text
@property
def order_date(self) -> str:
"""Return the order date.
:return: the order date
:rtype: str
"""
return self._get_value(
locator=self._order_date_locator,
field='textContent')
@property
def order_number(self) -> str:
"""Return the order identification number.
:return: the order number
:rtype: str
"""
return self._get_value(
locator=self._order_number_locator,
field='textContent')
@property
def product(self) -> str:
"""Return the purchased product name.
:return: the product name
:rtype: str
"""
return self._get_value(
locator=self._product_name_locator,
field='textContent')
@property
def price(self) -> str:
"""Return the product price.
:return: the product price
:rtype: str
"""
return self._get_value(
locator=self._product_price_locator,
field='textContent')
@property
def tax_type(self) -> str:
"""Return the tax type.
:return: the type of tax being applied
:rtype: str
"""
return self._get_value(
locator=self._tax_type_locator,
field='textContent')
@property
def tax(self) -> str:
"""Return the tax total.
:return: the total tax applied to the order
:rtype: str
"""
return self._get_value(
locator=self._tax_total_locator,
field='textContent')
@property
def total(self) -> str:
"""Return the order's total cost.
:return: the total cost of the purchase
:rtype: str
"""
return self._get_value(
locator=self._sales_total_locator,
field='textContent')
[docs] def access_your_course(self) -> StudentCourse:
"""Click the 'Access your course' continuation button.
:return: the student course page
:rtype: :py:class:`~pages.tutor.course.StudentCourse`
"""
confirmation = self.find_element(*self._base_iframe_locator)
self.driver.switch_to.frame(confirmation)
button = self.find_element(*self._access_your_course_button_locator)
is_not_chrome = not Utility.is_browser(self.driver, 'chrome')
Utility.click_option(
self.driver, element=button, force_js_click=is_not_chrome)
self.driver.switch_to.default_content()
return go_to_(StudentCourse(self.driver, base_url=self.page.base_url))
# -------------------------------------------------------- #
# Assignment shared properties
# -------------------------------------------------------- #
[docs]class Enrollment(Page):
"""The standard student course enrollment (direct URL signup)."""
URL_TEMPLATE = '/enroll/{enrollment_code}/{course_name}-{term}-{year}'
_splash_content_locator = (By.CSS_SELECTOR, '.splash')
_get_started_button_locator = (By.CSS_SELECTOR, 'a')
@property
def loaded(self) -> bool:
"""Return True if the enrollment introduction is loaded.
:return: ``True`` if the enrollment introduction is loaded, otherwise
``False``
:rtype: bool
"""
return bool(self.content)
@property
def content(self) -> str:
"""Return the splash text content.
:return: the enrollment introductory text
:rtype: str
"""
return (self.find_element(*self._splash_content_locator)
.get_attribute('textContent'))
[docs] def get_started(self) -> Union[AccountSignup, Enrollment.StudentID]:
"""Click on the 'Get Started' button to begin enrollment.
:return: the account signup flow for new users or the student ID
assignment for logged in users
:rtype: :py:class:`~pages.accounts.signup.Signup` or
:py:class:`~pages.tutor.enrollment.Enrollment.StudentID`
"""
button = self.find_element(*self._get_started_button_locator)
Utility.click_option(self.driver, element=button)
sleep(1)
if 'accounts' in self.driver.current_url:
return AccountSignup(self.driver)
return StudentID(self.driver, base_url=self.base_url)
[docs]class StudentID(Page):
"""Enter the student's identification number."""
URL_TEMPLATE = '/enroll/start/{enrollment_code}'
_student_id_icon_locator = (By.CSS_SELECTOR, '.student-id-icon')
_course_name_locator = (By.CSS_SELECTOR, '.title h4')
_student_id_input_locator = (By.CSS_SELECTOR, '.inputs input')
_continue_button_locator = (By.CSS_SELECTOR, '.btn-success')
_add_it_later_link_locator = (By.CSS_SELECTOR, '.cancel')
@property
def loaded(self) -> bool:
"""Return True when the student ID badge element is found.
:return: ``True`` when the student ID icon is found
:rtype: bool
"""
return bool(self.find_elements(*self._student_id_icon_locator))
@property
def course_name(self) -> str:
"""Return the course name associated with the enrollment code.
:return: the course name
:rtype: str
"""
return self.find_element(*self._course_name_locator).text
@property
def student_id(self) -> WebElement:
"""Return the student ID field.
:return: the student ID field
:rtype: :py:class:`~selenium.webdriver.remote.webelement.WebElement`
"""
return self.find_element(*self._student_id_input_locator)
@student_id.setter
def student_id(self, _id: str) -> None:
"""Set the student ID.
:param str _id: the student's identification number
:return: None
"""
return self.student_id.send_keys(_id)
def _continue(self, add_it_later: bool = False) \
-> Union[PrivacyPolicy, BuyAccess, StudentCourse]:
"""Click on the 'Continue' button.
After clicking on the continue button, one of three destinations are
possible:
1) the student course page with the privacy policy modal open when
the student is new or has not accepted the privacy policy
2) the student course page with the product purchase modal open for
paid courses
3) the student course page without a modal open for existing
students in a free course
:param bool add_it_later: (optional) click the 'Add it later' link
instead of the 'Continue' button
:return: the course page with the privacy policy or product purchase
modal displayed
:rtype: :py:class:`~pages.tutor.enrollment.PrivacyPolicy` or
:py:class:`~pages.tutor.enrollment.BuyAccess` or
:py:class:`~pages.tutor.course.StudentCourse`
"""
locator = self._continue_button_locator if not add_it_later \
else self._add_it_later_link_locator
button = self.find_element(*locator)
Utility.click_option(self.driver, element=button)
sleep(1.25)
course = StudentCourse(self.driver, base_url=self.base_url)
for _ in range(5):
dialog_root = self.driver.execute_script(GET_ROOT.format('dialog'))
sleep(1)
if dialog_root:
break
sleep(1)
if 'Privacy Policy' in self.driver.page_source:
return PrivacyPolicy(course, dialog_root)
elif dialog_root:
return BuyAccess(course, dialog_root)
return go_to_(course)
[docs] def add_it_later(self) -> Union[PrivacyPolicy, BuyAccess]:
"""Click on the 'Add it later' link.
After clicking on the add it later button, one of two destinations are
possible:
1. the student course page with the privacy policy modal open
#. the student course page with the product purchse modal open
:return: the course page with the privacy policy or product purchse
modal displayed
:rtype: :py:class:`PrivacyPolicy` or :py:class:`BuyAccess`
"""
return self._continue(add_it_later=True)
[docs]class Terms(TutorBase):
"""The terms of use and privacy policy acceptance page."""
@property
def loaded(self) -> bool:
"""Return True when the policies are displayed.
:return: ``True`` when the terms of use and privacy policy are shown.
:rtype: bool
"""
content = self.modal.heading.lower()
return 'terms' in content and 'privacy' in content
@property
def modal(self) -> PrivacyPolicy:
"""Access the Terms modal.
:return: the combined policy modal
:rtype: :py:class:`~pages.tutor.enrollment.PrivacyPolicy`
"""
return PrivacyPolicy(self)
[docs] def i_agree(self) -> StudentID:
"""Agree to the terms of use and the privacy policy.
:return: the student identification number entry
:rtype: :py:class:`pages.tutor.enrollment.StudentID`
"""
self.modal.i_agree()
return go_to_(StudentID(self.driver, base_url=self.base_url))