Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions edxapp_selenium_pages/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

4 changes: 4 additions & 0 deletions edxapp_selenium_pages/lms/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import os

# Get the URL of the instance under test
BASE_URL = os.environ.get('test_url', '')
41 changes: 41 additions & 0 deletions edxapp_selenium_pages/lms/course_about.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from bok_choy.page_object import PageObject
from ..lms import BASE_URL


class CourseAboutPage(PageObject):
"""
Course about page (with registration button)
"""

@property
def name(self):
return "lms.course_about"

@property
def requirejs(self):
return []

@property
def js_globals(self):
return []

def url(self, course_id=None):
"""
URL for the about page of a course.
Course ID is currently of the form "edx/999/2013_Spring"
but this format could change.
"""
if course_id is None:
raise NotImplemented("Must provide a course ID to access about page")

return BASE_URL + "/courses/" + course_id + "about"

def is_browser_on_page(self):
return self.is_css_present('section.course-info')

def register(self):
"""
Register for the course on the page.
"""
self.css_click('a.register')
self.ui.wait_for_page('lms.register')
42 changes: 42 additions & 0 deletions edxapp_selenium_pages/lms/course_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from bok_choy.page_object import PageObject
from ..lms import BASE_URL


class CourseInfoPage(PageObject):
"""
Course info.
"""

@property
def name(self):
return "lms.course_info"

@property
def requirejs(self):
return []

@property
def js_globals(self):
return []

def url(self, course_id=None):
"""
Go directly to the course info page for `course_id`.
(e.g. "edX/Open_DemoX/edx_demo_course")
"""
return BASE_URL + "/courses/" + course_id + "/info"

def is_browser_on_page(self):
return self.is_css_present('section.updates')

def num_updates(self):
"""
Return the number of updates on the page.
"""
return self.css_count('section.updates ol li')

def handout_links(self):
"""
Return a list of handout assets links.
"""
return self.css_map('section.handouts ol li a', lambda el: el['href'])
203 changes: 203 additions & 0 deletions edxapp_selenium_pages/lms/course_nav.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise, fulfill_after
from ..lms import BASE_URL


class CourseNavPage(PageObject):
"""
Navigate sections and sequences in the courseware.
"""

@property
def name(self):
return "lms.course_nav"

@property
def requirejs(self):
return []

@property
def js_globals(self):
return []

def url(self, **kwargs):
"""
Since course navigation appears on multiple pages,
it doesn't have a particular URL.
"""
raise NotImplemented

def is_browser_on_page(self):
return self.is_css_present('section.course-index')

@property
def sections(self):
"""
Return a dictionary representation of sections and subsections.

Example:

{
'Introduction': ['Course Overview'],
'Week 1': ['Lesson 1', 'Lesson 2', 'Homework']
'Final Exam': ['Final Exam']
}

You can use these titles in `go_to_section` to navigate to the section.
"""
# Dict to store the result
nav_dict = dict()

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]

if len(section_titles) < 1:
self.warning("Could not find subsections for '{0}'".format(sec_title))
else:
# Add one to convert list index (starts at 0) to CSS index (starts at 1)
nav_dict[sec_title] = self._subsection_titles(sec_index + 1)

return nav_dict

@property
def sequence_items(self):
"""
Return a list of sequence items on the page.
Sequence items are one level below subsections in the course nav.

Example return value:
['Chemical Bonds Video', 'Practice Problems', 'Homework']
"""
seq_css = 'ol#sequence-list>li>a>p'
return self.css_map(seq_css, lambda el: el.html.strip().split('\n')[0])

def go_to_section(self, section_title, subsection_title):
"""
Go to the section in the courseware.
Every section must have at least one subsection, so specify
both the section and subsection title.

Example:
go_to_section("Week 1", "Lesson 1")
"""

# For test stability, disable JQuery animations (opening / closing menus)
self.disable_jquery_animations()

# Get the section by index
try:
sec_index = self._section_titles().index(section_title)
except ValueError:
self.warning("Could not find section '{0}'".format(section_title))
return

# 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)

# Get the subsection by index
try:
subsec_index = self._subsection_titles(sec_index + 1).index(subsection_title)
except ValueError:
msg = "Could not find subsection '{0}' in section '{1}'".format(subsection_title, section_title)
self.warning(msg)
return

# Convert list indices (start at zero) to CSS indices (start at 1)
subsection_css = "nav>div.chapter:nth-of-type({0})>ul>li:nth-of-type({1})>a".format(
sec_index + 1, subsec_index + 1
)

# 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)

def go_to_sequential(self, sequential_title):
"""
Within a section/subsection, navigate to the sequential with `sequential_title`.
"""

# Get the index of the item in the sequence
all_items = self.sequence_items

try:
seq_index = all_items.index(sequential_title)

except ValueError:
msg = "Could not find sequential '{0}'. Available sequentials: [{1}]".format(
sequential_title, ", ".join(all_items)
)
self.warning(msg)

else:

# 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)

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())

def _subsection_titles(self, section_index):
"""
Return a list of all subsection titles on the page
for the section at index `section_index` (starts at 1).
"""
# Retrieve the subsection title for the section
# Add one to the list index to get the CSS index, which starts at one
subsection_css = 'nav>div.chapter:nth-of-type({0})>ul>li>a>p:nth-of-type(1)'.format(section_index)

# If the element is visible, we can get its text directly
# 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()
)

def _on_section_promise(self, section_title, subsection_title):
"""
Return a `Promise` that is fulfilled when the user is on
the correct section and subsection.
"""
desc = "currently at section '{0}' and subsection '{1}'".format(section_title, subsection_title)
return EmptyPromise(
lambda: self._is_on_section(section_title, subsection_title), desc
)

def _is_on_section(self, section_title, subsection_title):
"""
Return a boolean indicating whether the user is on the section and subsection
with the specified titles.

This assumes that the currently expanded section is the one we're on
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')

if len(current_section_list) == 0:
self.warning("Could not find the current section")
return False

elif len(current_subsection_list) == 0:
self.warning("Could not find current subsection")
return False

else:
return (
current_section_list[0].strip() == section_title and
current_subsection_list[0].strip().split('\n')[0] == subsection_title
)
62 changes: 62 additions & 0 deletions edxapp_selenium_pages/lms/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from bok_choy.page_object import PageObject
from ..lms import BASE_URL


class DashboardPage(PageObject):
"""
Student dashboard, where the student can view
courses she/he has registered for.
"""

@property
def name(self):
return "lms.dashboard"

@property
def requirejs(self):
return []

@property
def js_globals(self):
return []

def url(self, **kwargs):
return BASE_URL + "/dashboard"

def is_browser_on_page(self):
return self.is_css_present('section.my-courses')

def available_courses(self):
"""
Return list of the names of available courses (e.g. "999 edX Demonstration Course")
"""
return self.css_text('section.info > hgroup > h3 > a')

def view_course(self, course_id):
"""
Go to the course with `course_id` (e.g. edx/Open_DemoX/edx_demo_course)
"""
link_css = self._link_css(course_id)

if link_css is not None:
self.css_click(link_css)
else:
msg = "No links found for course {0}".format(course_id)
self.warning(msg)

def _link_css(self, course_id):

# Get the link hrefs for all courses
all_links = self.css_map('a.enter-course', lambda el: el['href'])

# Search for the first link that matches the course id
link_index = None
for index in range(len(all_links)):
if course_id in all_links[index]:
link_index = index
break

if link_index is not None:
return "a.enter-course:nth-of-type({0})".format(link_index + 1)
else:
return None
Loading