Commit eae4adeb by Will Daly

Merge pull request #2361 from edx/will/bok-choy-upgrade-and-lms-tests

Bok-choy upgrade, LMS tests, cleanup
parents 5e48aece e859a370
...@@ -2,33 +2,28 @@ ...@@ -2,33 +2,28 @@
Course about page (with registration button) Course about page (with registration button)
""" """
from bok_choy.page_object import PageObject from .course_page import CoursePage
from . import BASE_URL from .register import RegisterPage
class CourseAboutPage(PageObject): class CourseAboutPage(CoursePage):
""" """
Course about page (with registration button) Course about page (with registration button)
""" """
name = "lms.course_about"
def url(self, course_id=None): #pylint: disable=W0221 URL_PATH = "about"
"""
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 NotImplementedError("Must provide a course ID to access about page")
return BASE_URL + "/courses/" + course_id + "/about"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('section.course-info') return self.is_css_present('section.course-info')
def register(self): def register(self):
""" """
Register for the course on the page. Navigate to the registration page.
Waits for the registration page to load, then
returns the registration page object.
""" """
self.css_click('a.register') self.css_click('a.register')
self.ui.wait_for_page('lms.register')
registration_page = RegisterPage(self.browser, self.course_id)
registration_page.wait_for_page()
return registration_page
...@@ -2,33 +2,27 @@ ...@@ -2,33 +2,27 @@
Course info page. Course info page.
""" """
from bok_choy.page_object import PageObject from .course_page import CoursePage
from . import BASE_URL
class CourseInfoPage(PageObject): class CourseInfoPage(CoursePage):
""" """
Course info. Course info.
""" """
name = "lms.course_info" URL_PATH = "info"
def url(self, course_id=None): #pylint: disable=W0221
"""
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): def is_browser_on_page(self):
return self.is_css_present('section.updates') return self.is_css_present('section.updates')
@property
def num_updates(self): def num_updates(self):
""" """
Return the number of updates on the page. Return the number of updates on the page.
""" """
return self.css_count('section.updates ol li') return self.css_count('section.updates ol li')
@property
def handout_links(self): def handout_links(self):
""" """
Return a list of handout assets links. Return a list of handout assets links.
......
...@@ -12,14 +12,7 @@ class CourseNavPage(PageObject): ...@@ -12,14 +12,7 @@ class CourseNavPage(PageObject):
Navigate sections and sequences in the courseware. Navigate sections and sequences in the courseware.
""" """
name = "lms.course_nav" url = None
def url(self, **kwargs):
"""
Since course navigation appears on multiple pages,
it doesn't have a particular URL.
"""
raise NotImplementedError
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('section.course-index') return self.is_css_present('section.course-index')
......
"""
Base class for pages in courseware.
"""
from bok_choy.page_object import PageObject
from . import BASE_URL
class CoursePage(PageObject):
"""
Abstract base class for page objects within a course.
"""
# Overridden by subclasses to provide the relative path within the course
# Paths should not include the leading forward slash.
URL_PATH = ""
def __init__(self, browser, course_id):
"""
Course ID is currently of the form "edx/999/2013_Spring"
but this format could change.
"""
super(CoursePage, self).__init__(browser)
self.course_id = course_id
@property
def url(self):
"""
Construct a URL to the page within the course.
"""
return BASE_URL + "/courses/" + self.course_id + "/" + self.URL_PATH
...@@ -12,19 +12,22 @@ class DashboardPage(PageObject): ...@@ -12,19 +12,22 @@ class DashboardPage(PageObject):
courses she/he has registered for. courses she/he has registered for.
""" """
name = "lms.dashboard" url = BASE_URL + "/dashboard"
def url(self, **kwargs):
return BASE_URL + "/dashboard"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('section.my-courses') return self.is_css_present('section.my-courses')
@property
def available_courses(self): def available_courses(self):
""" """
Return list of the names of available courses (e.g. "999 edX Demonstration Course") Return list of the names of available courses (e.g. "999 edX Demonstration Course")
""" """
return self.css_text('section.info > hgroup > h3 > a') def _get_course_name(el):
# The first component in the link text is the course number
_, course_name = el.text.split(' ', 1)
return course_name
return self.css_map('section.info > hgroup > h3 > a', _get_course_name)
def view_course(self, course_id): def view_course(self, course_id):
""" """
......
...@@ -12,50 +12,15 @@ class FindCoursesPage(PageObject): ...@@ -12,50 +12,15 @@ class FindCoursesPage(PageObject):
Find courses page (main page of the LMS). Find courses page (main page of the LMS).
""" """
name = "lms.find_courses" url = BASE_URL
def url(self):
return BASE_URL
def is_browser_on_page(self): def is_browser_on_page(self):
return self.browser.title == "edX" return self.browser.title == "edX"
@property
def course_id_list(self): def course_id_list(self):
""" """
Retrieve the list of available course IDs Retrieve the list of available course IDs
on the page. on the page.
""" """
return self.css_map('article.course', lambda el: el['id']) return self.css_map('article.course', lambda el: el['id'])
def go_to_course(self, course_id):
"""
Navigate to the course with `course_id`.
Currently the course id has the form
edx/999/2013_Spring, but this could change.
"""
# Try clicking the link directly
try:
css = 'a[href="/courses/{0}/about"]'.format(course_id)
# In most browsers, there are multiple links
# that match this selector, most without text
# In IE 10, only the second one works.
# In IE 9, there is only one link
if self.css_count(css) > 1:
index = 1
else:
index = 0
self.css_click(css + ":nth-of-type({0})".format(index))
# Chrome gives an error that another element would receive the click.
# So click higher up in the DOM
except BrokenPromise:
# We need to escape forward slashes in the course_id
# to create a valid CSS selector
course_id = course_id.replace('/', r'\/')
self.css_click('article.course#{0}'.format(course_id))
# Ensure that we end up on the next page
self.ui.wait_for_page('lms.course_about')
...@@ -12,10 +12,7 @@ class LoginPage(PageObject): ...@@ -12,10 +12,7 @@ class LoginPage(PageObject):
Login page for the LMS. Login page for the LMS.
""" """
name = "lms.login" url = BASE_URL + "/login"
def url(self):
return BASE_URL + "/login"
def is_browser_on_page(self): def is_browser_on_page(self):
return any([ return any([
......
...@@ -11,13 +11,7 @@ class OpenResponsePage(PageObject): ...@@ -11,13 +11,7 @@ class OpenResponsePage(PageObject):
Open-ended response in the courseware. Open-ended response in the courseware.
""" """
name = "lms.open_response" url = None
def url(self):
"""
Open-response isn't associated with a particular URL.
"""
raise NotImplementedError
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('section.xmodule_CombinedOpenEndedModule') return self.is_css_present('section.xmodule_CombinedOpenEndedModule')
...@@ -117,6 +111,19 @@ class OpenResponsePage(PageObject): ...@@ -117,6 +111,19 @@ class OpenResponsePage(PageObject):
return map(map_feedback, labels) return map(map_feedback, labels)
@property @property
def written_feedback(self):
"""
Return the written feedback from the grader (if any).
If no feedback available, returns None.
"""
feedback = self.css_text('div.written-feedback')
if len(feedback) > 0:
return feedback[0]
else:
return None
@property
def alert_message(self): def alert_message(self):
""" """
Alert message displayed to the user. Alert message displayed to the user.
......
...@@ -2,19 +2,15 @@ ...@@ -2,19 +2,15 @@
Student progress page Student progress page
""" """
from bok_choy.page_object import PageObject from .course_page import CoursePage
from . import BASE_URL
class ProgressPage(PageObject): class ProgressPage(CoursePage):
""" """
Student progress page. Student progress page.
""" """
name = "lms.progress" URL_PATH = "progress"
def url(self, course_id=None): #pylint: disable=W0221
return BASE_URL + "/courses/" + course_id + "/progress"
def is_browser_on_page(self): def is_browser_on_page(self):
has_course_info = self.is_css_present('section.course-info') has_course_info = self.is_css_present('section.course-info')
......
...@@ -4,6 +4,7 @@ Registration page (create a new account) ...@@ -4,6 +4,7 @@ Registration page (create a new account)
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from . import BASE_URL from . import BASE_URL
from .dashboard import DashboardPage
class RegisterPage(PageObject): class RegisterPage(PageObject):
...@@ -11,18 +12,23 @@ class RegisterPage(PageObject): ...@@ -11,18 +12,23 @@ class RegisterPage(PageObject):
Registration page (create a new account) Registration page (create a new account)
""" """
name = "lms.register" def __init__(self, browser, course_id):
def url(self, course_id=None): #pylint: disable=W0221
""" """
URL for the registration page of a course.
Course ID is currently of the form "edx/999/2013_Spring" Course ID is currently of the form "edx/999/2013_Spring"
but this format could change. but this format could change.
""" """
if course_id is None: super(RegisterPage, self).__init__(browser)
raise NotImplementedError("Must provide a course ID to access about page") self._course_id = course_id
return BASE_URL + "/register?course_id=" + course_id + "&enrollment_action=enroll" def url(self):
"""
URL for the registration page of a course.
"""
return "{base}/register?course_id={course_id}&enrollment_action={action}".format(
base=BASE_URL,
course_id=self._course_id,
action="enroll",
)
def is_browser_on_page(self): def is_browser_on_page(self):
return any([ return any([
...@@ -30,16 +36,15 @@ class RegisterPage(PageObject): ...@@ -30,16 +36,15 @@ class RegisterPage(PageObject):
for title in self.css_text('span.title-sub') for title in self.css_text('span.title-sub')
]) ])
def provide_info(self, credentials): def provide_info(self, email, password, username, full_name):
""" """
Fill in registration info. Fill in registration info.
`email`, `password`, `username`, and `full_name` are the user's credentials.
`credentials` is a `TestCredential` object.
""" """
self.css_fill('input#email', credentials.email) self.css_fill('input#email', email)
self.css_fill('input#password', credentials.password) self.css_fill('input#password', password)
self.css_fill('input#username', credentials.username) self.css_fill('input#username', username)
self.css_fill('input#name', credentials.full_name) self.css_fill('input#name', full_name)
self.css_check('input#tos-yes') self.css_check('input#tos-yes')
self.css_check('input#honorcode-yes') self.css_check('input#honorcode-yes')
...@@ -48,3 +53,8 @@ class RegisterPage(PageObject): ...@@ -48,3 +53,8 @@ class RegisterPage(PageObject):
Submit registration info to create an account. Submit registration info to create an account.
""" """
self.css_click('button#submit') self.css_click('button#submit')
# The next page is the dashboard; make sure it loads
dashboard = DashboardPage(self.browser)
dashboard.wait_for_page()
return dashboard
...@@ -11,14 +11,7 @@ class TabNavPage(PageObject): ...@@ -11,14 +11,7 @@ class TabNavPage(PageObject):
High-level tab navigation. High-level tab navigation.
""" """
name = "lms.tab_nav" url = None
def url(self, **kwargs):
"""
Since tab navigation appears on multiple pages,
it doesn't have a particular URL.
"""
raise NotImplementedError
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('ol.course-tabs') return self.is_css_present('ol.course-tabs')
...@@ -40,6 +33,19 @@ class TabNavPage(PageObject): ...@@ -40,6 +33,19 @@ class TabNavPage(PageObject):
else: else:
self.warning("No tabs found for '{0}'".format(tab_name)) self.warning("No tabs found for '{0}'".format(tab_name))
def is_on_tab(self, tab_name):
"""
Return a boolean indicating whether the current tab is `tab_name`.
"""
current_tab_list = self.css_text('ol.course-tabs>li>a.active')
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)
def _tab_css(self, tab_name): def _tab_css(self, tab_name):
""" """
Return the CSS to click for `tab_name`. Return the CSS to click for `tab_name`.
...@@ -58,19 +64,6 @@ class TabNavPage(PageObject): ...@@ -58,19 +64,6 @@ class TabNavPage(PageObject):
Return a `Promise` that the user is on the tab `tab_name`. Return a `Promise` that the user is on the tab `tab_name`.
""" """
return EmptyPromise( return EmptyPromise(
lambda: self._is_on_tab(tab_name), lambda: self.is_on_tab(tab_name),
"{0} is the current tab".format(tab_name) "{0} is the current tab".format(tab_name)
) )
def _is_on_tab(self, tab_name):
"""
Return a boolean indicating whether the current tab is `tab_name`.
"""
current_tab_list = self.css_text('ol.course-tabs>li>a.active')
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)
...@@ -5,20 +5,16 @@ Video player in the courseware. ...@@ -5,20 +5,16 @@ Video player in the courseware.
import time import time
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise, fulfill_after from bok_choy.promise import EmptyPromise, fulfill_after
from bok_choy.javascript import wait_for_js, js_defined
@js_defined('window.Video')
class VideoPage(PageObject): class VideoPage(PageObject):
""" """
Video player in the courseware. Video player in the courseware.
""" """
name = "lms.video" url = None
def url(self):
"""
Video players aren't associated with a particular URL.
"""
raise NotImplementedError
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('section.xmodule_VideoModule') return self.is_css_present('section.xmodule_VideoModule')
...@@ -53,22 +49,20 @@ class VideoPage(PageObject): ...@@ -53,22 +49,20 @@ class VideoPage(PageObject):
""" """
return self.is_css_present('a.video_control') and self.is_css_present('a.video_control.play') return self.is_css_present('a.video_control') and self.is_css_present('a.video_control.play')
@wait_for_js
def play(self): def play(self):
""" """
Start playing the video. Start playing the video.
""" """
with fulfill_after( with fulfill_after(EmptyPromise(lambda: self.is_playing, "Video is playing")):
EmptyPromise(lambda: self.is_playing, "Video is playing")
):
self.css_click('a.video_control.play') self.css_click('a.video_control.play')
@wait_for_js
def pause(self): def pause(self):
""" """
Pause the video. Pause the video.
""" """
with fulfill_after( with fulfill_after(EmptyPromise(lambda: self.is_paused, "Video is paused")):
EmptyPromise(lambda: self.is_paused, "Video is paused")
):
self.css_click('a.video_control.pause') self.css_click('a.video_control.pause')
def _video_time(self): def _video_time(self):
......
...@@ -2,28 +2,15 @@ ...@@ -2,28 +2,15 @@
The Files and Uploads page for a course in Studio The Files and Uploads page for a course in Studio
""" """
from bok_choy.page_object import PageObject from .course_page import CoursePage
from .helpers import parse_course_id
from . import BASE_URL
class AssetIndexPage(PageObject): class AssetIndexPage(CoursePage):
""" """
The Files and Uploads page for a course in Studio The Files and Uploads page for a course in Studio
""" """
name = "studio.uploads" URL_PATH = "assets"
def url(self, course_id=None): #pylint: disable=W0221
"""
URL to the files and uploads page for a course.
`course_id` is a string of the form "org.number.run", and it is required
"""
_, _, course_run = parse_course_id(course_id)
return "{0}/assets/{1}/branch/draft/block/{2}".format(
BASE_URL, course_id, course_run
)
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-uploads') return self.is_css_present('body.view-uploads')
...@@ -14,9 +14,7 @@ class AutoAuthPage(PageObject): ...@@ -14,9 +14,7 @@ class AutoAuthPage(PageObject):
this url will create a user and log them in. this url will create a user and log them in.
""" """
name = "studio.auto_auth" def __init__(self, browser, username=None, email=None, password=None, staff=None, course_id=None):
def url(self, username=None, email=None, password=None, staff=None, course_id=None): #pylint: disable=W0221
""" """
Auto-auth is an end-point for HTTP GET requests. Auto-auth is an end-point for HTTP GET requests.
By default, it will create accounts with random user credentials, By default, it will create accounts with random user credentials,
...@@ -29,31 +27,34 @@ class AutoAuthPage(PageObject): ...@@ -29,31 +27,34 @@ class AutoAuthPage(PageObject):
Note that "global staff" is NOT the same as course staff. Note that "global staff" is NOT the same as course staff.
""" """
super(AutoAuthPage, self).__init__(browser)
# The base URL, used for creating a random user
url = BASE_URL + "/auto_auth"
# Create query string parameters if provided # Create query string parameters if provided
params = {} self._params = {}
if username is not None: if username is not None:
params['username'] = username self._params['username'] = username
if email is not None: if email is not None:
params['email'] = email self._params['email'] = email
if password is not None: if password is not None:
params['password'] = password self._params['password'] = password
if staff is not None: if staff is not None:
params['staff'] = "true" if staff else "false" self._params['staff'] = "true" if staff else "false"
if course_id is not None: if course_id is not None:
params['course_id'] = course_id self._params['course_id'] = course_id
query_str = urllib.urlencode(params) @property
def url(self):
"""
Construct the URL.
"""
url = BASE_URL + "/auto_auth"
query_str = urllib.urlencode(self._params)
# Append the query string to the base URL
if query_str: if query_str:
url += "?" + query_str url += "?" + query_str
......
...@@ -2,28 +2,15 @@ ...@@ -2,28 +2,15 @@
Course checklists page. Course checklists page.
""" """
from bok_choy.page_object import PageObject from .course_page import CoursePage
from .helpers import parse_course_id
from . import BASE_URL
class ChecklistsPage(PageObject): class ChecklistsPage(CoursePage):
""" """
Course Checklists page. Course Checklists page.
""" """
name = "studio.checklists" URL_PATH = "checklists"
def url(self, course_id=None): # pylint: disable=W0221
"""
URL to the checklist page in a course.
`course_id` is a string of the form "org.number.run", and it is required
"""
_, _, course_run = parse_course_id(course_id)
return "{0}/checklists/{1}/branch/draft/block/{2}".format(
BASE_URL, course_id, course_run
)
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-checklists') return self.is_css_present('body.view-checklists')
...@@ -2,28 +2,15 @@ ...@@ -2,28 +2,15 @@
Course Import page. Course Import page.
""" """
from bok_choy.page_object import PageObject from .course_page import CoursePage
from .helpers import parse_course_id
from . import BASE_URL
class ImportPage(PageObject): class ImportPage(CoursePage):
""" """
Course Import page. Course Import page.
""" """
name = "studio.import" URL_PATH = "import"
def url(self, course_id=None): #pylint: disable=W0221
"""
URL for the import page of a course.
`course_id` is a string of the form "org.number.run" and is required.
"""
_, _, course_run = parse_course_id(course_id)
return "{0}/import/{1}/branch/draft/block/{2}".format(
BASE_URL, course_id, course_run
)
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-import') return self.is_css_present('body.view-import')
...@@ -2,28 +2,15 @@ ...@@ -2,28 +2,15 @@
Course Updates page. Course Updates page.
""" """
from bok_choy.page_object import PageObject from .course_page import CoursePage
from .helpers import parse_course_id
from . import BASE_URL
class CourseUpdatesPage(PageObject): class CourseUpdatesPage(CoursePage):
""" """
Course Updates page. Course Updates page.
""" """
name = "studio.updates" URL_PATH = "course_info"
def url(self, course_id=None): #pylint: disable=W0221
"""
URL for the course team page of a course.
`course_id` is a string of the form "org.number.run" and is required.
"""
_, _, course_run = parse_course_id(course_id)
return "{0}/course_info/{1}/branch/draft/block/{2}".format(
BASE_URL, course_id, course_run
)
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-updates') return self.is_css_present('body.view-updates')
"""
Base class for pages specific to a course in Studio.
"""
from bok_choy.page_object import PageObject
from . import BASE_URL
class CoursePage(PageObject):
"""
Abstract base class for page objects specific to a course in Studio.
"""
# Overridden by subclasses to provide the relative path within the course
# Does not need to include the leading forward or trailing slash
URL_PATH = ""
def __init__(self, browser, course_org, course_num, course_run):
"""
Initialize the page object for the course located at
`{course_org}.{course_num}.{course_run}`
These identifiers will likely change in the future.
"""
super(CoursePage, self).__init__(browser)
self.course_info = {
'course_org': course_org,
'course_num': course_num,
'course_run': course_run
}
@property
def url(self):
"""
Construct a URL to the page within the course.
"""
return "/".join([
BASE_URL, self.URL_PATH,
"{course_org}.{course_num}.{course_run}".format(**self.course_info),
"branch", "draft", "block", self.course_info['course_run']
])
...@@ -10,10 +10,5 @@ class SubsectionPage(PageObject): ...@@ -10,10 +10,5 @@ class SubsectionPage(PageObject):
Edit Subsection page in Studio Edit Subsection page in Studio
""" """
name = "studio.subsection"
def url(self):
raise NotImplementedError
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-subsection') return self.is_css_present('body.view-subsection')
...@@ -2,28 +2,15 @@ ...@@ -2,28 +2,15 @@
Static Pages page for a course. Static Pages page for a course.
""" """
from bok_choy.page_object import PageObject from .course_page import CoursePage
from .helpers import parse_course_id
from . import BASE_URL
class StaticPagesPage(PageObject): class StaticPagesPage(CoursePage):
""" """
Static Pages page for a course. Static Pages page for a course.
""" """
name = "studio.tabs" URL_PATH = "tabs"
def url(self, course_id=None): #pylint: disable=W0221
"""
URL to the static pages UI in a course.
`course_id` is a string of the form "org.number.run", and it is required
"""
_, _, course_run = parse_course_id(course_id)
return "{0}/tabs/{1}/branch/draft/block/{2}".format(
BASE_URL, course_id, course_run
)
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-static-pages') return self.is_css_present('body.view-static-pages')
...@@ -2,28 +2,15 @@ ...@@ -2,28 +2,15 @@
Course Export page. Course Export page.
""" """
from bok_choy.page_object import PageObject from .course_page import CoursePage
from .helpers import parse_course_id
from . import BASE_URL
class ExportPage(PageObject): class ExportPage(CoursePage):
""" """
Course Export page. Course Export page.
""" """
name = "studio.export" URL_PATH = "export"
def url(self, course_id=None): #pylint: disable=W0221
"""
URL for the export page of a course.
`course_id` is a string of the form "org.number.run" and is required.
"""
_, _, course_run = parse_course_id(course_id)
return "{0}/export/{1}/branch/draft/block/{2}".format(
BASE_URL, course_id, course_run
)
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-export') return self.is_css_present('body.view-export')
"""
Helper functions for Studio page objects.
"""
class InvalidCourseID(Exception):
"""
The course ID does not have the correct format.
"""
pass
def parse_course_id(course_id):
"""
Parse a `course_id` string of the form "org.number.run"
and return the components as a tuple.
Raises an `InvalidCourseID` exception if the course ID is not in the right format.
"""
if course_id is None:
raise InvalidCourseID("Invalid course ID: '{0}'".format(course_id))
elements = course_id.split('.')
# You need at least 3 parts to a course ID: org, number, and run
if len(elements) < 3:
raise InvalidCourseID("Invalid course ID: '{0}'".format(course_id))
return tuple(elements)
...@@ -11,10 +11,7 @@ class HowitworksPage(PageObject): ...@@ -11,10 +11,7 @@ class HowitworksPage(PageObject):
Home page for Studio when not logged in. Home page for Studio when not logged in.
""" """
name = "studio.howitworks" url = BASE_URL + "/howitworks"
def url(self):
return BASE_URL + "/howitworks"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-howitworks') return self.is_css_present('body.view-howitworks')
...@@ -11,10 +11,7 @@ class DashboardPage(PageObject): ...@@ -11,10 +11,7 @@ class DashboardPage(PageObject):
My Courses page in Studio My Courses page in Studio
""" """
name = "studio.dashboard" url = BASE_URL + "/course"
def url(self):
return BASE_URL + "/course"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-dashboard') return self.is_css_present('body.view-dashboard')
...@@ -12,10 +12,7 @@ class LoginPage(PageObject): ...@@ -12,10 +12,7 @@ class LoginPage(PageObject):
Login page for Studio. Login page for Studio.
""" """
name = "studio.login" url = BASE_URL + "/signin"
def url(self):
return BASE_URL + "/signin"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-signin') return self.is_css_present('body.view-signin')
......
...@@ -2,28 +2,15 @@ ...@@ -2,28 +2,15 @@
Course Team page in Studio. Course Team page in Studio.
""" """
from bok_choy.page_object import PageObject from .course_page import CoursePage
from .helpers import parse_course_id
from . import BASE_URL
class CourseTeamPage(PageObject): class CourseTeamPage(CoursePage):
""" """
Course Team page in Studio. Course Team page in Studio.
""" """
name = "studio.team" URL_PATH = "course_team"
def url(self, course_id=None): #pylint: disable=W0221
"""
URL for the course team page of a course.
`course_id` is a string of the form "org.number.run" and is required.
"""
_, _, course_run = parse_course_id(course_id)
return "{0}/course_team/{1}/branch/draft/block/{2}".format(
BASE_URL, course_id, course_run
)
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-team') return self.is_css_present('body.view-team')
...@@ -2,28 +2,15 @@ ...@@ -2,28 +2,15 @@
Course Outline page in Studio. Course Outline page in Studio.
""" """
from bok_choy.page_object import PageObject from .course_page import CoursePage
from .helpers import parse_course_id
from . import BASE_URL
class CourseOutlinePage(PageObject): class CourseOutlinePage(CoursePage):
""" """
Course Outline page in Studio. Course Outline page in Studio.
""" """
name = "studio.outline" URL_PATH = "course"
def url(self, course_id=None): #pylint: disable=W0221
"""
URL for the course team page of a course.
`course_id` is a string of the form "org.number.run" and is required.
"""
_, _, course_run = parse_course_id(course_id)
return "{0}/course/{1}/branch/draft/block/{2}".format(
BASE_URL, course_id, course_run
)
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-outline') return self.is_css_present('body.view-outline')
...@@ -2,28 +2,15 @@ ...@@ -2,28 +2,15 @@
Course Schedule and Details Settings page. Course Schedule and Details Settings page.
""" """
from bok_choy.page_object import PageObject from .course_page import CoursePage
from .helpers import parse_course_id
from . import BASE_URL
class SettingsPage(PageObject): class SettingsPage(CoursePage):
""" """
Course Schedule and Details Settings page. Course Schedule and Details Settings page.
""" """
name = "studio.settings" URL_PATH = "settings/details"
def url(self, course_id=None): #pylint: disable=W0221
"""
URL for the settings page of a particular course.
`course_id` is a string of the form "org.number.run" and is required.
"""
_, _, course_run = parse_course_id(course_id)
return "{0}/settings/details/{1}/branch/draft/block/{2}".format(
BASE_URL, course_id, course_run
)
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-settings') return self.is_css_present('body.view-settings')
...@@ -2,28 +2,15 @@ ...@@ -2,28 +2,15 @@
Course Advanced Settings page Course Advanced Settings page
""" """
from bok_choy.page_object import PageObject from .course_page import CoursePage
from .helpers import parse_course_id
from . import BASE_URL
class AdvancedSettingsPage(PageObject): class AdvancedSettingsPage(CoursePage):
""" """
Course Advanced Settings page. Course Advanced Settings page.
""" """
name = "studio.advanced" URL_PATH = "settings/advanced"
def url(self, course_id=None): #pylint: disable=W0221
"""
URL to the advanced setting page in a course.
`course_id` is a string of the form "org.number.run", and it is required
"""
_, _, course_run = parse_course_id(course_id)
return "{0}/settings/advanced/{1}/branch/draft/block/{2}".format(
BASE_URL, course_id, course_run
)
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.advanced') return self.is_css_present('body.advanced')
...@@ -2,28 +2,15 @@ ...@@ -2,28 +2,15 @@
Course Grading Settings page. Course Grading Settings page.
""" """
from bok_choy.page_object import PageObject from .course_page import CoursePage
from .helpers import parse_course_id
from . import BASE_URL
class GradingPage(PageObject): class GradingPage(CoursePage):
""" """
Course Grading Settings page. Course Grading Settings page.
""" """
name = "studio.grading" URL_PATH = "settings/grading"
def url(self, course_id=None): #pylint: disable=W0221
"""
URL for the course team page of a course.
`course_id` is a string of the form "org.number.run" and is required.
"""
_, _, course_run = parse_course_id(course_id)
return "{0}/settings/grading/{1}/branch/draft/block/{2}".format(
BASE_URL, course_id, course_run
)
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.grading') return self.is_css_present('body.grading')
...@@ -7,10 +7,7 @@ class SignupPage(PageObject): ...@@ -7,10 +7,7 @@ class SignupPage(PageObject):
Signup page for Studio. Signup page for Studio.
""" """
name = "studio.signup" url = BASE_URL + "/signup"
def url(self):
return BASE_URL + "/signup"
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-signup') return self.is_css_present('body.view-signup')
...@@ -2,28 +2,15 @@ ...@@ -2,28 +2,15 @@
Course Textbooks page. Course Textbooks page.
""" """
from bok_choy.page_object import PageObject from .course_page import CoursePage
from .helpers import parse_course_id
from . import BASE_URL
class TextbooksPage(PageObject): class TextbooksPage(CoursePage):
""" """
Course Textbooks page. Course Textbooks page.
""" """
name = "studio.textbooks" URL_PATH = "textbooks"
def url(self, course_id=None): #pylint: disable=W0221
"""
URL to the textbook UI in a course.
`course_id` is a string of the form "org.number.run", and it is required
"""
_, _, course_run = parse_course_id(course_id)
return "{0}/textbooks/{1}/branch/draft/block/{2}".format(
BASE_URL, course_id, course_run
)
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-textbooks') return self.is_css_present('body.view-textbooks')
...@@ -10,10 +10,5 @@ class UnitPage(PageObject): ...@@ -10,10 +10,5 @@ class UnitPage(PageObject):
Unit page in Studio Unit page in Studio
""" """
name = "studio.unit"
def url(self):
raise NotImplementedError
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-unit') return self.is_css_present('body.view-unit')
"""
Base fixtures.
"""
from bok_choy.web_app_fixture import WebAppFixture
from django.core.management import call_command
class DjangoCmdFixture(WebAppFixture):
"""
Install a fixture by executing a Django management command.
"""
def __init__(self, cmd, *args, **kwargs):
"""
Configure the fixture to call `cmd` with the specified
positional and keyword arguments.
"""
self._cmd = cmd
self._args = args
self._kwargs = kwargs
def install(self):
"""
Call the Django management command.
"""
# We do not catch exceptions here. Since management commands
# execute arbitrary Python code, any exception could be raised.
# So it makes sense to let those go all the way up to the test runner,
# where they can quickly be found and fixed.
call_command(self._cmd, *self._args, **self._kwargs)
...@@ -4,11 +4,10 @@ Fixture to configure XQueue response. ...@@ -4,11 +4,10 @@ Fixture to configure XQueue response.
import requests import requests
import json import json
from bok_choy.web_app_fixture import WebAppFixture, WebAppFixtureError
from . import XQUEUE_STUB_URL from . import XQUEUE_STUB_URL
class XQueueResponseFixture(WebAppFixture): class XQueueResponseFixture(object):
""" """
Configure the XQueue stub's response to submissions. Configure the XQueue stub's response to submissions.
""" """
......
<problem>
<script type="loncapa/python">
z = "A*x^2 + sqrt(y)"
</script>
<startouttext/>
<p>Some edX courses ask you to enter an algebraic expression as an answer. Try entering the following algebraic expression in the box below. It’s easier than it looks.</p>
<p> \(A \cdot x^2 + \sqrt{y}\)
</p>
<p>
The entry is case sensitive. The product must be indicated with an asterisk, and the exponentiation with a caret, so you would write
"A*x^2 + sqrt(y)".</p>
<endouttext/>
<formularesponse type="cs" samples="A,x,y@1,1,1:3,3,3#10" answer="$z">
<responseparam description="Numerical Tolerance" type="tolerance" default="0.00001" name="tol"/>
<textline size="40" math="1"/>
</formularesponse>
</problem>
<problem markdown="Many edX courses have homework or exercises you need to complete. Notice the clock image to the left? That means this homework or exercise needs to be completed for you to pass the course. (This can be a bit confusing; the exercise may or may not have a due date prior to the end of the course.)&#10;&#10;We’ve provided eight (8) examples of how a professor might ask you questions. While the multiple choice question types below are somewhat standard, explore the other question types in the sequence above, like the formula builder- try them all out. &#10;&#10;As you go through the question types, notice how edX gives you immediate feedback on your responses - it really helps in the learning process.&#10;&#10;What color is the open ocean on a sunny day?&#10;&#10;[[yellow, (blue), green]]&#10;&#10;&#10;Which piece of furniture is built for sitting?&#10;&#10;( ) a table&#10;( ) a desk&#10;(x) a chair&#10;( ) a bookshelf&#10;&#10;Which of the following are musical instruments?&#10;&#10;[x] a piano&#10;[ ] a tree&#10;[x] a guitar&#10;[ ] a window&#10;&#10;&#10; " max_attempts="" weight="">
<p>Many edX courses have homework or exercises you need to complete. Notice the clock image to the left? That means this homework or exercise needs to be completed for you to pass the course. (This can be a bit confusing; the exercise may or may not have a due date prior to the end of the course.)</p>
<p>We’ve provided eight (8) examples of how a professor might ask you questions. While the multiple choice question types below are somewhat standard, explore the other question types in the sequence above, like the formula builder- try them all out. </p>
<p>As you go through the question types, notice how edX gives you immediate feedback on your responses - it really helps in the learning process.</p>
<p>What color is the open ocean on a sunny day?</p>
<optionresponse>
<optioninput options="('yellow','blue','green')" correct="blue"/>
</optionresponse>
<p>Which piece of furniture is built for sitting?</p>
<multiplechoiceresponse>
<choicegroup type="MultipleChoice">
<choice correct="false">a table</choice>
<choice correct="false">a desk</choice>
<choice correct="true">a chair</choice>
<choice correct="false">a bookshelf</choice>
</choicegroup>
</multiplechoiceresponse>
<p>Which of the following are musical instruments?</p>
<choiceresponse>
<checkboxgroup direction="vertical">
<choice correct="true">a piano</choice>
<choice correct="false">a tree</choice>
<choice correct="true">a guitar</choice>
<choice correct="false">a window</choice>
</checkboxgroup>
</choiceresponse>
<p> </p>
</problem>
"""
E2E tests for the LMS.
"""
from bok_choy.web_app_test import WebAppTest
from bok_choy.promise import EmptyPromise, fulfill_before
from .helpers import UniqueCourseTest, load_data_str
from ..edxapp_pages.studio.auto_auth import AutoAuthPage
from ..edxapp_pages.lms.login import LoginPage
from ..edxapp_pages.lms.find_courses import FindCoursesPage
from ..edxapp_pages.lms.course_about import CourseAboutPage
from ..edxapp_pages.lms.register import RegisterPage
from ..edxapp_pages.lms.course_info import CourseInfoPage
from ..edxapp_pages.lms.tab_nav import TabNavPage
from ..edxapp_pages.lms.course_nav import CourseNavPage
from ..edxapp_pages.lms.progress import ProgressPage
from ..edxapp_pages.lms.video import VideoPage
from ..fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc
class RegistrationTest(UniqueCourseTest):
"""
Test the registration process.
"""
def setUp(self):
"""
Initialize pages and install a course fixture.
"""
super(RegistrationTest, self).setUp()
self.find_courses_page = FindCoursesPage(self.browser)
self.course_about_page = CourseAboutPage(self.browser, self.course_id)
# Create a course to register for
course_fix = CourseFixture(
self.course_info['org'], self.course_info['number'],
self.course_info['run'], self.course_info['display_name']
).install()
def test_register(self):
# Visit the main page with the list of courses
self.find_courses_page.visit()
# Expect that the fixture course exists
course_ids = self.find_courses_page.course_id_list
self.assertIn(self.course_id, course_ids)
# Go to the course about page and click the register button
self.course_about_page.visit()
register_page = self.course_about_page.register()
# Fill in registration info and submit
username = "test_" + self.unique_id[0:6]
register_page.provide_info(
username + "@example.com", "test", username, "Test User"
)
dashboard = register_page.submit()
# We should end up at the dashboard
# Check that we're registered for the course
course_names = dashboard.available_courses
self.assertIn(self.course_info['display_name'], course_names)
class HighLevelTabTest(UniqueCourseTest):
"""
Tests that verify each of the high-level tabs available within a course.
"""
def setUp(self):
"""
Initialize pages and install a course fixture.
"""
super(HighLevelTabTest, self).setUp()
self.course_info_page = CourseInfoPage(self.browser, self.course_id)
self.progress_page = ProgressPage(self.browser, self.course_id)
self.course_nav = CourseNavPage(self.browser)
self.tab_nav = TabNavPage(self.browser)
self.video = VideoPage(self.browser)
# Install a course with sections/problems, tabs, updates, and handouts
course_fix = CourseFixture(
self.course_info['org'], self.course_info['number'],
self.course_info['run'], self.course_info['display_name']
)
course_fix.add_update(
CourseUpdateDesc(date='January 29, 2014', content='Test course update')
)
course_fix.add_handout('demoPDF.pdf')
course_fix.add_children(
XBlockFixtureDesc('static_tab', 'Test Static Tab'),
XBlockFixtureDesc('chapter', 'Test Section').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
XBlockFixtureDesc('problem', 'Test Problem 1', data=load_data_str('multiple_choice.xml')),
XBlockFixtureDesc('problem', 'Test Problem 2', data=load_data_str('formula_problem.xml')),
XBlockFixtureDesc('html', 'Test HTML'),
)),
XBlockFixtureDesc('chapter', 'Test Section 2').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection 2'),
XBlockFixtureDesc('sequential', 'Test Subsection 3'),
)).install()
# Auto-auth register for the course
AutoAuthPage(self.browser, course_id=self.course_id).visit()
def test_course_info(self):
"""
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')
# Expect just one update
self.assertEqual(self.course_info_page.num_updates, 1)
# Expect a link to the demo handout pdf
handout_links = self.course_info_page.handout_links
self.assertEqual(len(handout_links), 1)
self.assertIn('demoPDF.pdf', handout_links[0])
def test_progress(self):
"""
Navigate to the progress page.
"""
# Navigate to the progress page from the info page
self.course_info_page.visit()
self.tab_nav.go_to_tab('Progress')
# We haven't answered any problems yet, so assume scores are zero
# Only problems should have scores; so there should be 2 scores.
CHAPTER = 'Test Section'
SECTION = 'Test Subsection'
EXPECTED_SCORES = [(0, 3), (0, 1)]
actual_scores = self.progress_page.scores(CHAPTER, SECTION)
self.assertEqual(actual_scores, EXPECTED_SCORES)
def test_static_tab(self):
"""
Navigate to a static tab (course content)
"""
# From the course info page, navigate to the static tab
self.course_info_page.visit()
self.tab_nav.go_to_tab('Test Static Tab')
self.assertTrue(self.tab_nav.is_on_tab('Test Static Tab'))
def test_courseware_nav(self):
"""
Navigate to a particular unit in the courseware.
"""
# Navigate to the courseware page from the info page
self.course_info_page.visit()
self.tab_nav.go_to_tab('Courseware')
# Check that the courseware navigation appears correctly
EXPECTED_SECTIONS = {
'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)
self.assertEqual(actual_sections[section], EXPECTED_SECTIONS[section])
# Navigate to a particular section
self.course_nav.go_to_section('Test Section', 'Test Subsection')
# Check the sequence items
EXPECTED_ITEMS = ['Test Problem 1', 'Test Problem 2', 'Test HTML']
actual_items = self.course_nav.sequence_items
self.assertEqual(len(actual_items), len(EXPECTED_ITEMS))
for expected in EXPECTED_ITEMS:
self.assertIn(expected, actual_items)
class VideoTest(UniqueCourseTest):
"""
Navigate to a video in the courseware and play it.
"""
def setUp(self):
"""
Initialize pages and install a course fixture.
"""
super(VideoTest, self).setUp()
self.course_info_page = CourseInfoPage(self.browser, self.course_id)
self.course_nav = CourseNavPage(self.browser)
self.tab_nav = TabNavPage(self.browser)
self.video = VideoPage(self.browser)
# Install a course fixture with a video component
course_fix = CourseFixture(
self.course_info['org'], self.course_info['number'],
self.course_info['run'], self.course_info['display_name']
)
course_fix.add_children(
XBlockFixtureDesc('chapter', 'Test Section').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
XBlockFixtureDesc('video', 'Video')
))).install()
# Auto-auth register for the course
AutoAuthPage(self.browser, course_id=self.course_id).visit()
def test_video_player(self):
"""
Play a video in the courseware.
"""
# Navigate to a video
self.course_info_page.visit()
self.tab_nav.go_to_tab('Courseware')
# The video should start off paused
# Since the video hasn't loaded yet, it's elapsed time is 0
self.assertFalse(self.video.is_playing)
self.assertEqual(self.video.elapsed_time, 0)
# Play the video
self.video.play()
# Now we should be playing
self.assertTrue(self.video.is_playing)
# 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):
# 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)
...@@ -23,36 +23,24 @@ class OpenResponseTest(UniqueCourseTest): ...@@ -23,36 +23,24 @@ class OpenResponseTest(UniqueCourseTest):
some helper functions used in the ORA tests. some helper functions used in the ORA tests.
""" """
page_object_classes = [
AutoAuthPage, CourseInfoPage, TabNavPage,
CourseNavPage, OpenResponsePage, ProgressPage
]
# Grade response (dict) to return from the XQueue stub # Grade response (dict) to return from the XQueue stub
# in response to our unique submission text. # in response to our unique submission text.
XQUEUE_GRADE_RESPONSE = None XQUEUE_GRADE_RESPONSE = None
def setUp(self): def setUp(self):
""" """
Install a test course with ORA problems.
Always start in the subsection with open response problems. Always start in the subsection with open response problems.
""" """
# Create a unique submission
self.submission = "Test submission " + self.unique_id
# Ensure fixtures are installed
super(OpenResponseTest, self).setUp() super(OpenResponseTest, self).setUp()
# Log in and navigate to the essay problems # Create page objects
self.ui.visit('studio.auto_auth', course_id=self.course_id) self.auth_page = AutoAuthPage(self.browser, course_id=self.course_id)
self.ui.visit('lms.course_info', course_id=self.course_id) self.course_info_page = CourseInfoPage(self.browser, self.course_id)
self.ui['lms.tab_nav'].go_to_tab('Courseware') self.tab_nav = TabNavPage(self.browser)
self.course_nav = CourseNavPage(self.browser)
@property self.open_response = OpenResponsePage(self.browser)
def fixtures(self): self.progress_page = ProgressPage(self.browser, self.course_id)
"""
Create a test course with open response problems.
Configure the XQueue stub to respond to submissions to the open-ended queue.
"""
# Configure the test course # Configure the test course
course_fix = CourseFixture( course_fix = CourseFixture(
...@@ -61,7 +49,6 @@ class OpenResponseTest(UniqueCourseTest): ...@@ -61,7 +49,6 @@ class OpenResponseTest(UniqueCourseTest):
) )
course_fix.add_children( course_fix.add_children(
XBlockFixtureDesc('chapter', 'Test Section').add_children( XBlockFixtureDesc('chapter', 'Test Section').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection').add_children( XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
...@@ -73,17 +60,21 @@ class OpenResponseTest(UniqueCourseTest): ...@@ -73,17 +60,21 @@ class OpenResponseTest(UniqueCourseTest):
XBlockFixtureDesc('combinedopenended', 'Peer-Assessed', XBlockFixtureDesc('combinedopenended', 'Peer-Assessed',
data=load_data_str('ora_peer_problem.xml'), metadata={'graded': True}), data=load_data_str('ora_peer_problem.xml'), metadata={'graded': True}),
)
) XBlockFixtureDesc('peergrading', 'Peer Module'),
)
))).install()
# Configure the XQueue stub's response for the text we will submit # Configure the XQueue stub's response for the text we will submit
# The submission text is unique so we can associate each response with a particular test case.
self.submission = "Test submission " + self.unique_id[0:4]
if self.XQUEUE_GRADE_RESPONSE is not None: if self.XQUEUE_GRADE_RESPONSE is not None:
xqueue_fix = XQueueResponseFixture(self.submission, self.XQUEUE_GRADE_RESPONSE) XQueueResponseFixture(self.submission, self.XQUEUE_GRADE_RESPONSE).install()
return [course_fix, xqueue_fix]
else: # Log in and navigate to the essay problems
return [course_fix] self.auth_page.visit()
self.course_info_page.visit()
self.tab_nav.go_to_tab('Courseware')
def submit_essay(self, expected_assessment_type, expected_prompt): def submit_essay(self, expected_assessment_type, expected_prompt):
""" """
...@@ -93,21 +84,21 @@ class OpenResponseTest(UniqueCourseTest): ...@@ -93,21 +84,21 @@ class OpenResponseTest(UniqueCourseTest):
""" """
# Check the assessment type and prompt # Check the assessment type and prompt
self.assertEqual(self.ui['lms.open_response'].assessment_type, expected_assessment_type) self.assertEqual(self.open_response.assessment_type, expected_assessment_type)
self.assertIn(expected_prompt, self.ui['lms.open_response'].prompt) self.assertIn(expected_prompt, self.open_response.prompt)
# Enter a submission, which will trigger a pre-defined response from the XQueue stub. # Enter a submission, which will trigger a pre-defined response from the XQueue stub.
self.ui['lms.open_response'].set_response(self.submission) self.open_response.set_response(self.submission)
# Save the response and expect some UI feedback # Save the response and expect some UI feedback
self.ui['lms.open_response'].save_response() self.open_response.save_response()
self.assertEqual( self.assertEqual(
self.ui['lms.open_response'].alert_message, self.open_response.alert_message,
"Answer saved, but not yet submitted." "Answer saved, but not yet submitted."
) )
# Submit the response # Submit the response
self.ui['lms.open_response'].submit_response() self.open_response.submit_response()
def get_asynch_feedback(self, assessment_type): def get_asynch_feedback(self, assessment_type):
""" """
...@@ -142,9 +133,9 @@ class OpenResponseTest(UniqueCourseTest): ...@@ -142,9 +133,9 @@ class OpenResponseTest(UniqueCourseTest):
raise ValueError('Assessment type not recognized. Must be either "ai" or "peer"') raise ValueError('Assessment type not recognized. Must be either "ai" or "peer"')
def _inner_check(): def _inner_check():
self.ui['lms.course_nav'].go_to_sequential('Self-Assessed') self.course_nav.go_to_sequential('Self-Assessed')
self.ui['lms.course_nav'].go_to_sequential(section_name) self.course_nav.go_to_sequential(section_name)
feedback = self.ui['lms.open_response'].rubric_feedback feedback = self.open_response.rubric_feedback
# Successful if `feedback` is a non-empty list # Successful if `feedback` is a non-empty list
return (bool(feedback), feedback) return (bool(feedback), feedback)
...@@ -165,27 +156,25 @@ class SelfAssessmentTest(OpenResponseTest): ...@@ -165,27 +156,25 @@ class SelfAssessmentTest(OpenResponseTest):
And I see my score in the progress page. And I see my score in the progress page.
""" """
# Navigate to the self-assessment problem and submit an essay # Navigate to the self-assessment problem and submit an essay
self.ui['lms.course_nav'].go_to_sequential('Self-Assessed') self.course_nav.go_to_sequential('Self-Assessed')
self.submit_essay('self', 'Censorship in the Libraries') self.submit_essay('self', 'Censorship in the Libraries')
# Check the rubric categories # Check the rubric categories
self.assertEqual( self.assertEqual(
self.ui['lms.open_response'].rubric_categories, self.open_response.rubric_categories, ["Writing Applications", "Language Conventions"]
["Writing Applications", "Language Conventions"]
) )
# Fill in the self-assessment rubric # Fill in the self-assessment rubric
self.ui['lms.open_response'].submit_self_assessment([0, 1]) self.open_response.submit_self_assessment([0, 1])
# Expect that we get feedback # Expect that we get feedback
self.assertEqual( self.assertEqual(
self.ui['lms.open_response'].rubric_feedback, self.open_response.rubric_feedback, ['incorrect', 'correct']
['incorrect', 'correct']
) )
# Verify the progress page # Verify the progress page
self.ui.visit('lms.progress', course_id=self.course_id) self.progress_page.visit()
scores = self.ui['lms.progress'].scores('Test Section', 'Test Subsection') scores = self.progress_page.scores('Test Section', 'Test Subsection')
# The first score is self-assessment, which we've answered, so it's 1/2 # The first score is self-assessment, which we've answered, so it's 1/2
# The other scores are AI- and peer-assessment, which we haven't answered so those are 0/2 # The other scores are AI- and peer-assessment, which we haven't answered so those are 0/2
...@@ -217,22 +206,16 @@ class AIAssessmentTest(OpenResponseTest): ...@@ -217,22 +206,16 @@ class AIAssessmentTest(OpenResponseTest):
""" """
# Navigate to the AI-assessment problem and submit an essay # Navigate to the AI-assessment problem and submit an essay
self.ui['lms.course_nav'].go_to_sequential('AI-Assessed') self.course_nav.go_to_sequential('AI-Assessed')
self.submit_essay('ai', 'Censorship in the Libraries') self.submit_essay('ai', 'Censorship in the Libraries')
# Expect UI feedback that the response was submitted
self.assertEqual(
self.ui['lms.open_response'].grader_status,
"Your response has been submitted. Please check back later for your grade."
)
# Refresh the page to get the updated feedback # Refresh the page to get the updated feedback
# then verify that we get the feedback sent by our stub XQueue implementation # then verify that we get the feedback sent by our stub XQueue implementation
self.assertEqual(self.get_asynch_feedback('ai'), ['incorrect', 'correct']) self.assertEqual(self.get_asynch_feedback('ai'), ['incorrect', 'correct'])
# Verify the progress page # Verify the progress page
self.ui.visit('lms.progress', course_id=self.course_id) self.progress_page.visit()
scores = self.ui['lms.progress'].scores('Test Section', 'Test Subsection') scores = self.progress_page.scores('Test Section', 'Test Subsection')
# First score is the self-assessment score, which we haven't answered, so it's 0/2 # First score is the self-assessment score, which we haven't answered, so it's 0/2
# Second score is the AI-assessment score, which we have answered, so it's 1/2 # Second score is the AI-assessment score, which we have answered, so it's 1/2
...@@ -286,23 +269,17 @@ class PeerFeedbackTest(OpenResponseTest): ...@@ -286,23 +269,17 @@ class PeerFeedbackTest(OpenResponseTest):
And I see my score in the progress page. And I see my score in the progress page.
""" """
# Navigate to the peer-assessment problem and submit an essay # Navigate to the peer-assessment problem and submit an essay
self.ui['lms.course_nav'].go_to_sequential('Peer-Assessed') self.course_nav.go_to_sequential('Peer-Assessed')
self.submit_essay('peer', 'Censorship in the Libraries') self.submit_essay('peer', 'Censorship in the Libraries')
# Expect UI feedback that the response was submitted
self.assertEqual(
self.ui['lms.open_response'].grader_status,
"Your response has been submitted. Please check back later for your grade."
)
# Refresh the page to get feedback from the stub XQueue grader. # Refresh the page to get feedback from the stub XQueue grader.
# We receive feedback from all three peers, each of which # We receive feedback from all three peers, each of which
# provide 2 scores (one for each rubric item) # provide 2 scores (one for each rubric item)
self.assertEqual(self.get_asynch_feedback('peer'), ['incorrect', 'correct'] * 3) self.assertEqual(self.get_asynch_feedback('peer'), ['incorrect', 'correct'] * 3)
# Verify the progress page # Verify the progress page
self.ui.visit('lms.progress', course_id=self.course_id) self.progress_page.visit()
scores = self.ui['lms.progress'].scores('Test Section', 'Test Subsection') scores = self.progress_page.scores('Test Section', 'Test Subsection')
# First score is the self-assessment score, which we haven't answered, so it's 0/2 # First score is the self-assessment score, which we haven't answered, so it's 0/2
# Second score is the AI-assessment score, which we haven't answered, so it's 0/2 # Second score is the AI-assessment score, which we haven't answered, so it's 0/2
......
...@@ -30,9 +30,9 @@ class LoggedOutTest(WebAppTest): ...@@ -30,9 +30,9 @@ class LoggedOutTest(WebAppTest):
Smoke test for pages in Studio that are visible when logged out. Smoke test for pages in Studio that are visible when logged out.
""" """
@property def setUp(self):
def page_object_classes(self): super(LoggedOutTest, self).setUp()
return [LoginPage, HowitworksPage, SignupPage] self.pages = [LoginPage(self.browser), HowitworksPage(self.browser), SignupPage(self.browser)]
def test_page_existence(self): def test_page_existence(self):
""" """
...@@ -40,8 +40,8 @@ class LoggedOutTest(WebAppTest): ...@@ -40,8 +40,8 @@ class LoggedOutTest(WebAppTest):
Rather than fire up the browser just to check each url, Rather than fire up the browser just to check each url,
do them all sequentially in this testcase. do them all sequentially in this testcase.
""" """
for page in ['login', 'howitworks', 'signup']: for page in self.pages:
self.ui.visit('studio.{0}'.format(page)) page.visit()
class LoggedInPagesTest(WebAppTest): class LoggedInPagesTest(WebAppTest):
...@@ -49,16 +49,18 @@ class LoggedInPagesTest(WebAppTest): ...@@ -49,16 +49,18 @@ class LoggedInPagesTest(WebAppTest):
Tests that verify the pages in Studio that you can get to when logged Tests that verify the pages in Studio that you can get to when logged
in and do not have a course yet. in and do not have a course yet.
""" """
@property
def page_object_classes(self): def setUp(self):
return [AutoAuthPage, DashboardPage] super(LoggedInPagesTest, self).setUp()
self.auth_page = AutoAuthPage(self.browser, staff=True)
self.dashboard_page = DashboardPage(self.browser)
def test_dashboard_no_courses(self): def test_dashboard_no_courses(self):
""" """
Make sure that you can get to the dashboard page without a course. Make sure that you can get to the dashboard page without a course.
""" """
self.ui.visit('studio.auto_auth', staff=True) self.auth_page.visit()
self.ui.visit('studio.dashboard') self.dashboard_page.visit()
class CoursePagesTest(UniqueCourseTest): class CoursePagesTest(UniqueCourseTest):
...@@ -69,23 +71,29 @@ class CoursePagesTest(UniqueCourseTest): ...@@ -69,23 +71,29 @@ class CoursePagesTest(UniqueCourseTest):
COURSE_ID_SEPARATOR = "." COURSE_ID_SEPARATOR = "."
@property def setUp(self):
def page_object_classes(self): """
return [ Install a course with no content using a fixture.
AutoAuthPage, AssetIndexPage, ChecklistsPage, ImportPage, CourseUpdatesPage, """
StaticPagesPage, ExportPage, CourseTeamPage, CourseOutlinePage, super(UniqueCourseTest, self).setUp()
SettingsPage, AdvancedSettingsPage, GradingPage, TextbooksPage
]
@property CourseFixture(
def fixtures(self):
course_fix = CourseFixture(
self.course_info['org'], self.course_info['org'],
self.course_info['number'], self.course_info['number'],
self.course_info['run'], self.course_info['run'],
self.course_info['display_name'] self.course_info['display_name']
) ).install()
return [course_fix]
self.auth_page = AutoAuthPage(self.browser, staff=True)
self.pages = [
clz(self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run'])
for clz in [
AssetIndexPage, ChecklistsPage, ImportPage, CourseUpdatesPage,
StaticPagesPage, ExportPage, CourseTeamPage, CourseOutlinePage, SettingsPage,
AdvancedSettingsPage, GradingPage, TextbooksPage
]
]
def test_page_existence(self): def test_page_existence(self):
""" """
...@@ -93,14 +101,9 @@ class CoursePagesTest(UniqueCourseTest): ...@@ -93,14 +101,9 @@ class CoursePagesTest(UniqueCourseTest):
Rather than fire up the browser just to check each url, Rather than fire up the browser just to check each url,
do them all sequentially in this testcase. do them all sequentially in this testcase.
""" """
pages = [
'uploads', 'checklists', 'import', 'updates', 'tabs', 'export',
'team', 'outline', 'settings', 'advanced', 'grading', 'textbooks'
]
# Log in # Log in
self.ui.visit('studio.auto_auth', staff=True) self.auth_page.visit()
# Verify that each page is available # Verify that each page is available
for page in pages: for page in self.pages:
self.ui.visit('studio.{0}'.format(page), course_id=self.course_id) page.visit()
...@@ -94,7 +94,7 @@ ...@@ -94,7 +94,7 @@
"password": "password", "password": "password",
"peer_grading": "peer_grading", "peer_grading": "peer_grading",
"staff_grading": "staff_grading", "staff_grading": "staff_grading",
"url": "http://localhost:18060/", "url": "** OVERRIDDEN **",
"username": "lms" "username": "lms"
}, },
"SECRET_KEY": "", "SECRET_KEY": "",
......
...@@ -41,6 +41,9 @@ XML_MODULESTORE['OPTIONS']['data_dir'] = (TEST_ROOT / "data").abspath() ...@@ -41,6 +41,9 @@ XML_MODULESTORE['OPTIONS']['data_dir'] = (TEST_ROOT / "data").abspath()
# Configure the LMS to use our stub XQueue implementation # Configure the LMS to use our stub XQueue implementation
XQUEUE_INTERFACE['url'] = 'http://localhost:8040' XQUEUE_INTERFACE['url'] = 'http://localhost:8040'
# Configure the LMS to use our stub ORA implementation
OPEN_ENDED_GRADING_INTERFACE['url'] = 'http://localhost:8041/'
# Enable django-pipeline and staticfiles # Enable django-pipeline and staticfiles
STATIC_ROOT = (TEST_ROOT / "staticfiles").abspath() STATIC_ROOT = (TEST_ROOT / "staticfiles").abspath()
PIPELINE = True PIPELINE = True
......
...@@ -30,7 +30,11 @@ BOK_CHOY_SERVERS = { ...@@ -30,7 +30,11 @@ BOK_CHOY_SERVERS = {
} }
BOK_CHOY_STUBS = { BOK_CHOY_STUBS = {
:xqueue => { :port => 8040, :log => File.join(BOK_CHOY_LOG_DIR, "bok_choy_xqueue.log") }
:xqueue => {
:port => 8040,
:log => File.join(BOK_CHOY_LOG_DIR, "bok_choy_xqueue.log")
}
} }
# For the time being, stubs are used by both the bok-choy and lettuce acceptance tests # For the time being, stubs are used by both the bok-choy and lettuce acceptance tests
...@@ -57,8 +61,8 @@ def start_servers() ...@@ -57,8 +61,8 @@ def start_servers()
) )
end end
end end
end
end
# Wait until we get a successful response from the servers or time out # Wait until we get a successful response from the servers or time out
def wait_for_test_servers() def wait_for_test_servers()
...@@ -162,12 +166,6 @@ namespace :'test:bok_choy' do ...@@ -162,12 +166,6 @@ namespace :'test:bok_choy' do
desc "Process assets and set up database for bok-choy tests" desc "Process assets and set up database for bok-choy tests"
task :setup => [:check_services, :install_prereqs, BOK_CHOY_LOG_DIR] do task :setup => [:check_services, :install_prereqs, BOK_CHOY_LOG_DIR] do
# Clear any test data already in Mongo
clear_mongo()
# Invalidate the cache
BOK_CHOY_CACHE.flush()
# Reset the database # Reset the database
sh("#{REPO_ROOT}/scripts/reset-test-db.sh") sh("#{REPO_ROOT}/scripts/reset-test-db.sh")
...@@ -182,20 +180,25 @@ namespace :'test:bok_choy' do ...@@ -182,20 +180,25 @@ namespace :'test:bok_choy' do
:check_services, BOK_CHOY_LOG_DIR, BOK_CHOY_REPORT_DIR, :clean_reports_dir :check_services, BOK_CHOY_LOG_DIR, BOK_CHOY_REPORT_DIR, :clean_reports_dir
] do |t, args| ] do |t, args|
# Clear any test data already in Mongo or MySQL and invalidate the cache
clear_mongo()
BOK_CHOY_CACHE.flush()
sh(django_admin('lms', 'bok_choy', 'flush', '--noinput'))
# Ensure the test servers are available # Ensure the test servers are available
puts "Starting test servers...".red puts "Starting test servers...".green
start_servers() start_servers()
puts "Waiting for servers to start...".red puts "Waiting for servers to start...".green
wait_for_test_servers() wait_for_test_servers()
begin begin
puts "Running test suite...".red puts "Running test suite...".green
run_bok_choy(args.test_spec) run_bok_choy(args.test_spec)
rescue rescue
puts "Tests failed!".red puts "Tests failed!".red
exit 1 exit 1
ensure ensure
puts "Cleaning up databases...".red puts "Cleaning up databases...".green
cleanup() cleanup()
end end
end end
......
...@@ -21,4 +21,4 @@ ...@@ -21,4 +21,4 @@
-e git+https://github.com/edx/js-test-tool.git@v0.1.5#egg=js_test_tool -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/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/event-tracking.git@f0211d702d#egg=event-tracking
-e git+https://github.com/edx/bok-choy.git@bc6f1adbe439618162079f1004b2b3db3b6f8916#egg=bok_choy -e git+https://github.com/edx/bok-choy.git@v0.1.0#egg=bok_choy
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