Commit 4afd5ea4 by Will Daly

Refactored bok-choy directory structure

Added fixtures for course and xblock creation
Added bok-choy Studio tests
Added bok-choy tests for ora self- and ai- assessment
Refactored auto-auth; added staff and course-enrollment options
Removed extra javascript properties from page objects
parent f3f0e8a5
# Settings for bok choy tests
"""
Settings for bok choy tests
"""
import os
from path import path
......@@ -11,9 +13,9 @@ from path import path
# This is a convenience for ensuring (a) that we can consistently find the files
# and (b) that the files are the same in Jenkins as in local dev.
os.environ['SERVICE_VARIANT'] = 'bok_choy'
os.environ['CONFIG_ROOT'] = path(__file__).abspath().dirname()
os.environ['CONFIG_ROOT'] = path(__file__).abspath().dirname() #pylint: disable=E1120
from aws import * # pylint: disable=W0401, W0614
from .aws import * # pylint: disable=W0401, W0614
######################### Testing overrides ####################################
......@@ -22,13 +24,13 @@ from aws import * # pylint: disable=W0401, W0614
INSTALLED_APPS += ('django_extensions',)
# Redirect to the test_root folder within the repo
TEST_ROOT = CONFIG_ROOT.dirname().dirname() / "test_root"
TEST_ROOT = CONFIG_ROOT.dirname().dirname() / "test_root" #pylint: disable=E1120
GITHUB_REPO_ROOT = (TEST_ROOT / "data").abspath()
LOG_DIR = (TEST_ROOT / "log").abspath()
# Configure Mongo modulestore to use the test folder within the repo
for store in ["default", "direct"]:
MODULESTORE[store]['OPTIONS']['fs_root'] = (TEST_ROOT / "data").abspath()
MODULESTORE[store]['OPTIONS']['fs_root'] = (TEST_ROOT / "data").abspath() #pylint: disable=E1120
# Enable django-pipeline and staticfiles
STATIC_ROOT = (TEST_ROOT / "staticfiles").abspath()
......@@ -37,10 +39,14 @@ PIPELINE = True
# Silence noisy logs
import logging
LOG_OVERRIDES = [
('track.middleware', logging.CRITICAL)
('track.middleware', logging.CRITICAL),
('edx.discussion', logging.CRITICAL),
]
for log_name, log_level in LOG_OVERRIDES:
logging.getLogger(log_name).setLevel(log_level)
# Use the auto_auth workflow for creating users and logging them in
FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True
# Unfortunately, we need to use debug mode to serve staticfiles
DEBUG = True
from django.test import TestCase
from django.test.client import Client
from django.contrib.auth.models import User
from student.models import CourseEnrollment, UserProfile
from util.testing import UrlResetMixin
from mock import patch
from django.core.urlresolvers import reverse, NoReverseMatch
......@@ -19,82 +20,101 @@ class AutoAuthEnabledTestCase(UrlResetMixin, TestCase):
# of the UrlResetMixin)
super(AutoAuthEnabledTestCase, self).setUp()
self.url = '/auto_auth'
self.cms_csrf_url = "signup"
self.lms_csrf_url = "signin_user"
self.client = Client()
def test_create_user(self):
"""
Test that user gets created when visiting the page.
"""
self._auto_auth()
self.assertEqual(User.objects.count(), 1)
self.assertTrue(User.objects.all()[0].is_active)
self.client.get(self.url)
def test_create_same_user(self):
self._auto_auth(username='test')
self._auto_auth(username='test')
self.assertEqual(User.objects.count(), 1)
qset = User.objects.all()
# assert user was created and is active
self.assertEqual(qset.count(), 1)
user = qset[0]
assert user.is_active
def test_create_multiple_users(self):
"""
Test to make sure multiple users are created.
"""
self._auto_auth()
self._auto_auth()
self.assertEqual(User.objects.all().count(), 2)
def test_create_defined_user(self):
"""
Test that the user gets created with the correct attributes
when they are passed as parameters on the auto-auth page.
"""
self.client.get(
self.url,
{'username': 'robot', 'password': 'test', 'email': 'robot@edx.org'}
self._auto_auth(
username='robot', password='test',
email='robot@edx.org', full_name="Robot Name"
)
qset = User.objects.all()
# assert user was created with the correct username and password
self.assertEqual(qset.count(), 1)
user = qset[0]
# Check that the user has the correct info
user = User.objects.get(username='robot')
self.assertEqual(user.username, 'robot')
self.assertTrue(user.check_password('test'))
self.assertEqual(user.email, 'robot@edx.org')
@patch('student.views.random.randint')
def test_create_multiple_users(self, randint):
"""
Test to make sure multiple users are created.
"""
randint.return_value = 1
self.client.get(self.url)
# Check that the user has a profile
user_profile = UserProfile.objects.get(user=user)
self.assertEqual(user_profile.name, "Robot Name")
# By default, the user should not be global staff
self.assertFalse(user.is_staff)
def test_create_staff_user(self):
randint.return_value = 2
self.client.get(self.url)
# Create a staff user
self._auto_auth(username='test', staff='true')
user = User.objects.get(username='test')
self.assertTrue(user.is_staff)
qset = User.objects.all()
# Revoke staff privileges
self._auto_auth(username='test', staff='false')
user = User.objects.get(username='test')
self.assertFalse(user.is_staff)
# make sure that USER_1 and USER_2 were created correctly
self.assertEqual(qset.count(), 2)
user1 = qset[0]
self.assertEqual(user1.username, 'USER_1')
self.assertTrue(user1.check_password('PASS_1'))
self.assertEqual(user1.email, 'USER_1_dummy_test@mitx.mit.edu')
self.assertEqual(qset[1].username, 'USER_2')
def test_course_enrollment(self):
@patch.dict("django.conf.settings.FEATURES", {"MAX_AUTO_AUTH_USERS": 1})
def test_login_already_created_user(self):
# Create a user and enroll in a course
course_id = "edX/Test101/2014_Spring"
self._auto_auth(username='test', course_id=course_id)
# Check that a course enrollment was created for the user
self.assertEqual(CourseEnrollment.objects.count(), 1)
enrollment = CourseEnrollment.objects.get(course_id=course_id)
self.assertEqual(enrollment.user.username, "test")
def test_double_enrollment(self):
# Create a user and enroll in a course
course_id = "edX/Test101/2014_Spring"
self._auto_auth(username='test', course_id=course_id)
# Make the same call again, re-enrolling the student in the same course
self._auto_auth(username='test', course_id=course_id)
# Check that only one course enrollment was created for the user
self.assertEqual(CourseEnrollment.objects.count(), 1)
enrollment = CourseEnrollment.objects.get(course_id=course_id)
self.assertEqual(enrollment.user.username, "test")
def _auto_auth(self, **params):
"""
Test that when we have reached the limit for automatic users
a subsequent request results in an already existant one being
logged in.
Make a request to the auto-auth end-point and check
that the response is successful.
"""
# auto-generate 1 user (the max)
url = '/auto_auth'
self.client.get(url)
# go to the site again
self.client.get(url)
qset = User.objects.all()
response = self.client.get(self.url, params)
self.assertEqual(response.status_code, 200)
# make sure it is the same user
self.assertEqual(qset.count(), 1)
# Check that session and CSRF are set in the response
for cookie in ['csrftoken', 'sessionid']:
self.assertIn(cookie, response.cookies) #pylint: disable=E1103
self.assertTrue(response.cookies[cookie].value) #pylint: disable=E1103
class AutoAuthDisabledTestCase(UrlResetMixin, TestCase):
......@@ -118,19 +138,3 @@ class AutoAuthDisabledTestCase(UrlResetMixin, TestCase):
"""
response = self.client.get(self.url)
self.assertEqual(response.status_code, 404)
def test_csrf_enabled(self):
"""
test that when not load testing, csrf protection is on
"""
cms_csrf_url = "signup"
lms_csrf_url = "signin_user"
self.client = Client(enforce_csrf_checks=True)
try:
csrf_protected_url = reverse(cms_csrf_url)
response = self.client.post(csrf_protected_url)
except NoReverseMatch:
csrf_protected_url = reverse(lms_csrf_url)
response = self.client.post(csrf_protected_url)
self.assertEqual(response.status_code, 403)
......@@ -981,54 +981,85 @@ def create_account(request, post_override=None):
def auto_auth(request):
"""
Automatically logs the user in with a generated random credentials
This view is only accessible when
settings.FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] is true.
"""
Create or configure a user account, then log in as that user.
def get_dummy_post_data(username, password, email, name):
"""
Return a dictionary suitable for passing to post_vars of _do_create_account or post_override
of create_account, with specified values.
"""
return {'username': username,
'email': email,
'password': password,
'name': name,
'honor_code': u'true',
'terms_of_service': u'true', }
Enabled only when
settings.FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] is true.
# generate random user credentials from a small name space (determined by settings)
name_base = 'USER_'
pass_base = 'PASS_'
Accepts the following querystring parameters:
* `username`, `email`, and `password` for the user account
* `full_name` for the user profile (the user's full name; defaults to the username)
* `staff`: Set to "true" to make the user global staff.
* `course_id`: Enroll the student in the course with `course_id`
max_users = settings.FEATURES.get('MAX_AUTO_AUTH_USERS', 200)
number = random.randint(1, max_users)
If username, email, or password are not provided, use
randomly generated credentials.
"""
# Get the params from the request to override default user attributes if specified
qdict = request.GET
# Generate a unique name to use if none provided
unique_name = uuid.uuid4().hex[0:30]
# Use the params from the request, otherwise use these defaults
username = qdict.get('username', name_base + str(number))
password = qdict.get('password', pass_base + str(number))
email = qdict.get('email', '%s_dummy_test@mitx.mit.edu' % username)
name = qdict.get('name', '%s Test' % username)
username = request.GET.get('username', unique_name)
password = request.GET.get('password', unique_name)
email = request.GET.get('email', unique_name + "@example.com")
full_name = request.GET.get('full_name', username)
is_staff = request.GET.get('staff', None)
course_id = request.GET.get('course_id', None)
# Get or create the user object
post_data = {
'username': username,
'email': email,
'password': password,
'name': full_name,
'honor_code': u'true',
'terms_of_service': u'true',
}
# if they already are a user, log in
try:
# Attempt to create the account.
# If successful, this will return a tuple containing
# the new user object; otherwise it will return an error
# message.
result = _do_create_account(post_data)
if isinstance(result, tuple):
user = result[0]
# If we did not create a new account, the user might already
# exist. Attempt to retrieve it.
else:
user = User.objects.get(username=username)
user = authenticate(username=username, password=password, request=request)
login(request, user)
user.email = email
user.set_password(password)
user.save()
# Set the user's global staff bit
if is_staff is not None:
user.is_staff = (is_staff == "true")
user.save()
# else create and activate account info
except ObjectDoesNotExist:
post_override = get_dummy_post_data(username, password, email, name)
create_account(request, post_override=post_override)
request.user.is_active = True
request.user.save()
# Activate the user
reg = Registration.objects.get(user=user)
reg.activate()
reg.save()
# return empty success
return HttpResponse('')
# Enroll the user in a course
if course_id is not None:
CourseEnrollment.enroll(user, course_id)
# Log in as the user
user = authenticate(username=username, password=password)
login(request, user)
# Provide the user with a valid CSRF token
# then return a 200 response
success_msg = u"Logged in user {0} ({1}) with password {2}".format(
username, email, password
)
response = HttpResponse(success_msg)
response.set_cookie('csrftoken', csrf(request)['csrf_token'])
return response
@ensure_csrf_cookie
......
# pylint: disable=C0111
# pylint: disable=W0621
import urllib
from lettuce import world
from django.contrib.auth.models import User, Group
from student.models import CourseEnrollment
......@@ -27,12 +28,13 @@ def create_user(uname, password):
@world.absorb
def log_in(username='robot', password='test', email='robot@edx.org', name='Robot'):
def log_in(username='robot', password='test', email='robot@edx.org', name="Robot"):
"""
Use the auto_auth feature to programmatically log the user in
"""
url = '/auto_auth?username=%s&password=%s&name=%s&email=%s' % (username,
password, name, email)
url = '/auto_auth'
params = { 'username': username, 'password': password, 'email': email, 'full_name': name }
url += "?" + urllib.urlencode(params)
world.visit(url)
# Save the user info in the world scenario_dict for use in the tests
......
"""
Course about page (with registration button)
"""
from bok_choy.page_object import PageObject
from ..lms import BASE_URL
from . import BASE_URL
class CourseAboutPage(PageObject):
"""
Course about page (with registration button)
"""
name = "lms.course_about"
@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):
def url(self, course_id=None): #pylint: disable=W0221
"""
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")
raise NotImplementedError("Must provide a course ID to access about page")
return BASE_URL + "/courses/" + course_id + "about"
return BASE_URL + "/courses/" + course_id + "/about"
def is_browser_on_page(self):
return self.is_css_present('section.course-info')
......
"""
Course info page.
"""
from bok_choy.page_object import PageObject
from ..lms import BASE_URL
from . import BASE_URL
class CourseInfoPage(PageObject):
......@@ -7,19 +11,9 @@ class CourseInfoPage(PageObject):
Course info.
"""
@property
def name(self):
return "lms.course_info"
@property
def requirejs(self):
return []
@property
def js_globals(self):
return []
name = "lms.course_info"
def url(self, course_id=None):
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")
......
"""
Course navigation page object
"""
import re
from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise, fulfill_after
from ..lms import BASE_URL
class CourseNavPage(PageObject):
......@@ -8,24 +12,14 @@ 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 []
name = "lms.course_nav"
def url(self, **kwargs):
"""
Since course navigation appears on multiple pages,
it doesn't have a particular URL.
"""
raise NotImplemented
raise NotImplementedError
def is_browser_on_page(self):
return self.is_css_present('section.course-index')
......@@ -73,7 +67,7 @@ class CourseNavPage(PageObject):
['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])
return self.css_map(seq_css, self._clean_seq_titles)
def go_to_section(self, section_title, subsection_title):
"""
......@@ -201,3 +195,12 @@ class CourseNavPage(PageObject):
current_section_list[0].strip() == section_title and
current_subsection_list[0].strip().split('\n')[0] == subsection_title
)
# Regular expression to remove HTML span tags from a string
REMOVE_SPAN_TAG_RE = re.compile(r'<span.+/span>')
def _clean_seq_titles(self, element):
"""
Clean HTML of sequence titles, stripping out span tags and returning the first line.
"""
return self.REMOVE_SPAN_TAG_RE.sub('', element.html).strip().split('\n')[0]
"""
Student dashboard page.
"""
from bok_choy.page_object import PageObject
from ..lms import BASE_URL
from . import BASE_URL
class DashboardPage(PageObject):
......@@ -8,17 +12,7 @@ class DashboardPage(PageObject):
courses she/he has registered for.
"""
@property
def name(self):
return "lms.dashboard"
@property
def requirejs(self):
return []
@property
def js_globals(self):
return []
name = "lms.dashboard"
def url(self, **kwargs):
return BASE_URL + "/dashboard"
......@@ -45,7 +39,9 @@ class DashboardPage(PageObject):
self.warning(msg)
def _link_css(self, course_id):
"""
Return a CSS selector for the link to the course with `course_id`.
"""
# Get the link hrefs for all courses
all_links = self.css_map('a.enter-course', lambda el: el['href'])
......
"""
Find courses page (main page of the LMS).
"""
from bok_choy.page_object import PageObject
from bok_choy.promise import BrokenPromise
from ..lms import BASE_URL
from . import BASE_URL
class FindCoursesPage(PageObject):
......@@ -8,17 +12,7 @@ class FindCoursesPage(PageObject):
Find courses page (main page of the LMS).
"""
@property
def name(self):
return "lms.find_courses"
@property
def requirejs(self):
return []
@property
def js_globals(self):
return []
name = "lms.find_courses"
def url(self):
return BASE_URL
......@@ -60,7 +54,7 @@ class FindCoursesPage(PageObject):
except BrokenPromise:
# We need to escape forward slashes in the course_id
# to create a valid CSS selector
course_id = course_id.replace('/', '\/')
course_id = course_id.replace('/', r'\/')
self.css_click('article.course#{0}'.format(course_id))
# Ensure that we end up on the next page
......
"""
Login page for the LMS.
"""
from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise, fulfill_after
from ..lms import BASE_URL
from . import BASE_URL
class LoginPage(PageObject):
......@@ -8,17 +12,7 @@ class LoginPage(PageObject):
Login page for the LMS.
"""
@property
def name(self):
return "lms.login"
@property
def requirejs(self):
return []
@property
def js_globals(self):
return []
name = "lms.login"
def url(self):
return BASE_URL + "/login"
......
"""
Open-ended response in the courseware.
"""
from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise, fulfill_after, fulfill_before
......@@ -7,23 +11,13 @@ class OpenResponsePage(PageObject):
Open-ended response in the courseware.
"""
@property
def name(self):
return "lms.open_response"
@property
def requirejs(self):
return []
@property
def js_globals(self):
return []
name = "lms.open_response"
def url(self):
"""
Open-response isn't associated with a particular URL.
"""
raise NotImplemented
raise NotImplementedError
def is_browser_on_page(self):
return self.is_css_present('section.xmodule_CombinedOpenEndedModule')
......@@ -49,7 +43,7 @@ class OpenResponsePage(PageObject):
elif 'peer' in label_compare:
return 'peer'
else:
raise ValueError("Unexpected assessment type: '{0}'".format(label))
raise ValueError("Unexpected assessment type: '{0}'".format(label_compare))
@property
def prompt(self):
......@@ -103,13 +97,16 @@ class OpenResponsePage(PageObject):
# We need to filter out the similar-looking CSS classes
# for the rubric items that are NOT marked correct/incorrect
feedback_css = 'div.rubric-label>label'
labels = filter(
lambda el_class: el_class != 'rubric-elements-info',
labels = [
el_class for el_class in
self.css_map(feedback_css, lambda el: el['class'])
)
if el_class != 'rubric-elements-info'
]
# Map CSS classes on the labels to correct/incorrect
def map_feedback(css_class):
"""
Map CSS classes on the labels to correct/incorrect
"""
if 'choicegroup_incorrect' in css_class:
return 'incorrect'
elif 'choicegroup_correct' in css_class:
......@@ -195,14 +192,14 @@ class OpenResponsePage(PageObject):
# Check that we have the enough radio buttons
category_css = "div.rubric>ul.rubric-list:nth-of-type({0})".format(score_index + 1)
if scores[score_index] > self.css_count(category_css + ' input.score-selection'):
msg = "Tried to select score {0} but there are only {1} options".format(score_num, len(inputs))
msg = "Tried to select score {0} but there are only {1} options".format(score_index, len(scores))
self.warning(msg)
# Check the radio button at the correct index
else:
input_css = (category_css +
">li.rubric-list-item:nth-of-type({0}) input.score-selection".format(
scores[score_index] + 1)
input_css = (
category_css +
">li.rubric-list-item:nth-of-type({0}) input.score-selection".format(scores[score_index] + 1)
)
self.css_check(input_css)
......
"""
Student progress page
"""
from bok_choy.page_object import PageObject
from ..lms import BASE_URL
from . import BASE_URL
class ProgressPage(PageObject):
......@@ -7,19 +11,9 @@ class ProgressPage(PageObject):
Student progress page.
"""
@property
def name(self):
return "lms.progress"
@property
def requirejs(self):
return []
@property
def js_globals(self):
return []
name = "lms.progress"
def url(self, course_id=None):
def url(self, course_id=None): #pylint: disable=W0221
return BASE_URL + "/courses/" + course_id + "/progress"
def is_browser_on_page(self):
......@@ -79,10 +73,10 @@ class ProgressPage(PageObject):
# The section titles also contain "n of m possible points" on the second line
# We have to remove this to find the right title
section_titles = [title.split('\n')[0] for title in section_titles]
section_titles = [t.split('\n')[0] for t in section_titles]
# Some links are blank, so remove them
section_titles = [title for title in section_titles if title]
section_titles = [t for t in section_titles if t]
try:
# CSS indices are 1-indexed, so add one to the list index
......
"""
Registration page (create a new account)
"""
from bok_choy.page_object import PageObject
from ..lms import BASE_URL
from . import BASE_URL
class RegisterPage(PageObject):
......@@ -7,26 +11,16 @@ class RegisterPage(PageObject):
Registration page (create a new account)
"""
@property
def name(self):
return "lms.register"
@property
def requirejs(self):
return []
@property
def js_globals(self):
return []
name = "lms.register"
def url(self, course_id=None):
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"
but this format could change.
"""
if course_id is None:
raise NotImplemented("Must provide a course ID to access about page")
raise NotImplementedError("Must provide a course ID to access about page")
return BASE_URL + "/register?course_id=" + course_id + "&enrollment_action=enroll"
......
"""
High-level tab navigation.
"""
from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise, fulfill_after
from ..lms import BASE_URL
class TabNavPage(PageObject):
......@@ -8,24 +11,14 @@ class TabNavPage(PageObject):
High-level tab navigation.
"""
@property
def name(self):
return "lms.tab_nav"
@property
def requirejs(self):
return []
@property
def js_globals(self):
return []
name = "lms.tab_nav"
def url(self, **kwargs):
"""
Since tab navigation appears on multiple pages,
it doesn't have a particular URL.
"""
raise NotImplemented
raise NotImplementedError
def is_browser_on_page(self):
return self.is_css_present('ol.course-tabs')
......
import time
"""
Video player in the courseware.
"""
import time
from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise, fulfill_after
from ..lms import BASE_URL
class VideoPage(PageObject):
......@@ -10,23 +12,13 @@ class VideoPage(PageObject):
Video player in the courseware.
"""
@property
def name(self):
return "lms.video"
@property
def requirejs(self):
return []
@property
def js_globals(self):
return []
name = "lms.video"
def url(self):
"""
Video players aren't associated with a particular URL.
"""
raise NotImplemented
raise NotImplementedError
def is_browser_on_page(self):
return self.is_css_present('section.xmodule_VideoModule')
......
"""
The Files and Uploads page for a course in Studio
"""
from bok_choy.page_object import PageObject
from .helpers import parse_course_id
from . import BASE_URL
class AssetIndexPage(PageObject):
"""
The Files and Uploads page for a course in Studio
"""
name = "studio.uploads"
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):
return self.is_css_present('body.view-uploads')
"""
Auto-auth page (used to automatically log in during testing).
"""
import urllib
from bok_choy.page_object import PageObject
from . import BASE_URL
class AutoAuthPage(PageObject):
"""
The automatic authorization page.
When allowed via the django settings file, visiting
this url will create a user and log them in.
"""
name = "studio.auto_auth"
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.
By default, it will create accounts with random user credentials,
but you can also specify credentials using querystring parameters.
`username`, `email`, and `password` are the user's credentials (strings)
`staff` is a boolean indicating whether the user is global staff.
`course_id` is the ID of the course to enroll the student in.
Currently, this has the form "org/number/run"
Note that "global staff" is NOT the same as course staff.
"""
# The base URL, used for creating a random user
url = BASE_URL + "/auto_auth"
# Create query string parameters if provided
params = {}
if username is not None:
params['username'] = username
if email is not None:
params['email'] = email
if password is not None:
params['password'] = password
if staff is not None:
params['staff'] = "true" if staff else "false"
if course_id is not None:
params['course_id'] = course_id
query_str = urllib.urlencode(params)
# Append the query string to the base URL
if query_str:
url += "?" + query_str
return url
def is_browser_on_page(self):
return True
"""
Course checklists page.
"""
from bok_choy.page_object import PageObject
from .helpers import parse_course_id
from . import BASE_URL
class ChecklistsPage(PageObject):
"""
Course Checklists page.
"""
name = "studio.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):
return self.is_css_present('body.view-checklists')
"""
Course Import page.
"""
from bok_choy.page_object import PageObject
from .helpers import parse_course_id
from . import BASE_URL
class ImportPage(PageObject):
"""
Course Import page.
"""
name = "studio.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):
return self.is_css_present('body.view-import')
"""
Course Updates page.
"""
from bok_choy.page_object import PageObject
from .helpers import parse_course_id
from . import BASE_URL
class CourseUpdatesPage(PageObject):
"""
Course Updates page.
"""
name = "studio.updates"
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):
return self.is_css_present('body.view-updates')
"""
Edit Subsection page in Studio
"""
from bok_choy.page_object import PageObject
class SubsectionPage(PageObject):
"""
Edit Subsection page in Studio
"""
name = "studio.subsection"
def url(self):
raise NotImplementedError
def is_browser_on_page(self):
return self.is_css_present('body.view-subsection')
"""
Static Pages page for a course.
"""
from bok_choy.page_object import PageObject
from .helpers import parse_course_id
from . import BASE_URL
class StaticPagesPage(PageObject):
"""
Static Pages page for a course.
"""
name = "studio.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):
return self.is_css_present('body.view-static-pages')
"""
Course Export page.
"""
from bok_choy.page_object import PageObject
from .helpers import parse_course_id
from . import BASE_URL
class ExportPage(PageObject):
"""
Course Export page.
"""
name = "studio.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):
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)
"""
Home page for Studio when not logged in.
"""
from bok_choy.page_object import PageObject
from . import BASE_URL
class HowitworksPage(PageObject):
"""
Home page for Studio when not logged in.
"""
name = "studio.howitworks"
def url(self):
return BASE_URL + "/howitworks"
def is_browser_on_page(self):
return self.is_css_present('body.view-howitworks')
"""
My Courses page in Studio
"""
from bok_choy.page_object import PageObject
from . import BASE_URL
class DashboardPage(PageObject):
"""
My Courses page in Studio
"""
name = "studio.dashboard"
def url(self):
return BASE_URL + "/course"
def is_browser_on_page(self):
return self.is_css_present('body.view-dashboard')
"""
Login page for Studio.
"""
from bok_choy.page_object import PageObject
from ..studio import BASE_URL
from bok_choy.promise import EmptyPromise, fulfill_after
from . import BASE_URL
class LoginPage(PageObject):
......@@ -7,28 +12,26 @@ class LoginPage(PageObject):
Login page for Studio.
"""
@property
def name(self):
return "studio.login"
@property
def requirejs(self):
return []
@property
def js_globals(self):
return []
name = "studio.login"
def url(self):
return BASE_URL + "/signin"
def is_browser_on_page(self):
return self.browser.title == 'Sign In | edX Studio'
return self.is_css_present('body.view-signin')
def login(self, email, password):
"""
Attempt to log in using `email` and `password`.
"""
self.css_fill('input#email', email)
self.css_fill('input#password', password)
self.css_click('button#submit')
# Ensure that we make it to another page
on_next_page = EmptyPromise(
lambda: "login" not in self.browser.url,
"redirected from the login page"
)
with fulfill_after(on_next_page):
self.css_fill('input#email', email)
self.css_fill('input#password', password)
self.css_click('button#submit')
"""
Course Team page in Studio.
"""
from bok_choy.page_object import PageObject
from .helpers import parse_course_id
from . import BASE_URL
class CourseTeamPage(PageObject):
"""
Course Team page in Studio.
"""
name = "studio.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):
return self.is_css_present('body.view-team')
"""
Course Outline page in Studio.
"""
from bok_choy.page_object import PageObject
from .helpers import parse_course_id
from . import BASE_URL
class CourseOutlinePage(PageObject):
"""
Course Outline page in Studio.
"""
name = "studio.outline"
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):
return self.is_css_present('body.view-outline')
"""
Course Schedule and Details Settings page.
"""
from bok_choy.page_object import PageObject
from .helpers import parse_course_id
from . import BASE_URL
class SettingsPage(PageObject):
"""
Course Schedule and Details Settings page.
"""
name = "studio.settings"
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):
return self.is_css_present('body.view-settings')
"""
Course Advanced Settings page
"""
from bok_choy.page_object import PageObject
from .helpers import parse_course_id
from . import BASE_URL
class AdvancedSettingsPage(PageObject):
"""
Course Advanced Settings page.
"""
name = "studio.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):
return self.is_css_present('body.advanced')
"""
Course Grading Settings page.
"""
from bok_choy.page_object import PageObject
from .helpers import parse_course_id
from . import BASE_URL
class GradingPage(PageObject):
"""
Course Grading Settings page.
"""
name = "studio.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):
return self.is_css_present('body.grading')
from bok_choy.page_object import PageObject
from ..studio import BASE_URL
from . import BASE_URL
class SignupPage(PageObject):
......@@ -7,20 +7,10 @@ class SignupPage(PageObject):
Signup page for Studio.
"""
@property
def name(self):
return "studio.signup"
@property
def requirejs(self):
return []
@property
def js_globals(self):
return []
name = "studio.signup"
def url(self):
return BASE_URL + "/signup"
def is_browser_on_page(self):
return self.browser.title == 'Sign Up | edX Studio'
return self.is_css_present('body.view-signup')
"""
Course Textbooks page.
"""
from bok_choy.page_object import PageObject
from .helpers import parse_course_id
from . import BASE_URL
class TextbooksPage(PageObject):
"""
Course Textbooks page.
"""
name = "studio.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):
return self.is_css_present('body.view-textbooks')
"""
Unit page in Studio
"""
from bok_choy.page_object import PageObject
class UnitPage(PageObject):
"""
Unit page in Studio
"""
name = "studio.unit"
def url(self):
raise NotImplementedError
def is_browser_on_page(self):
return self.is_css_present('body.view-unit')
import os
# Get the URL of the instance under test
STUDIO_BASE_URL = os.environ.get('studio_url', 'http://localhost:8031')
"""
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)
import logging
# Silence noisy loggers
LOG_OVERRIDES = [
('requests.packages.urllib3.connectionpool', logging.ERROR),
('django.db.backends', logging.ERROR)
]
for log_name, log_level in LOG_OVERRIDES:
logging.getLogger(log_name).setLevel(log_level)
<combinedopenended max_score="2" markdown="null" max_attempts="1000">
<rubric>
<rubric>
<category>
<description>Writing Applications</description>
<option> The essay loses focus, has little information or supporting details, and the organization makes it difficult to follow.</option>
<option> The essay presents a mostly unified theme, includes sufficient information to convey the theme, and is generally organized well.</option>
</category>
<category>
<description> Language Conventions </description>
<option> The essay demonstrates a reasonable command of proper spelling and grammar. </option>
<option> The essay demonstrates superior command of proper spelling and grammar.</option>
</category>
</rubric>
</rubric>
<prompt>
<h4>Censorship in the Libraries</h4>
<p>"All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us." --Katherine Paterson, Author</p>
<p>Write a persuasive essay to a newspaper reflecting your vies on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading.</p>
</prompt>
<task>
<openended>
<openendedparam>
<initial_display>Enter essay here.</initial_display>
<answer_display>This is the answer.</answer_display>
<grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload>
</openendedparam>
</openended>
</task>
</combinedopenended>
<combinedopenended max_score="1" accept_file_upload="False" markdown="null" max_attempts="1000" skip_spelling_checks="False" version="1">
<rubric>
<rubric>
<category>
<description>Writing Applications</description>
<option> The essay loses focus, has little information or supporting details, and the organization makes it difficult to follow.</option>
<option> The essay presents a mostly unified theme, includes sufficient information to convey the theme, and is generally organized well.</option>
</category>
<category>
<description> Language Conventions </description>
<option> The essay demonstrates a reasonable command of proper spelling and grammar. </option>
<option> The essay demonstrates superior command of proper spelling and grammar.</option>
</category>
</rubric>
</rubric>
<prompt>
<h4>Censorship in the Libraries</h4>
<p>"All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us." --Katherine Paterson, Author</p>
<p>Write a persuasive essay to a newspaper reflecting your views on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading.</p>
</prompt>
<task>
<selfassessment/>
</task>
</combinedopenended>
"""
Test helper functions.
"""
from path import path
def load_data_str(rel_path):
"""
Load a file from the "data" directory as a string.
`rel_path` is the path relative to the data directory.
"""
full_path = path(__file__).abspath().dirname() / "data" / rel_path #pylint: disable=E1120
with open(full_path) as data_file:
return data_file.read()
"""
Tests for ORA (Open Response Assessment) through the LMS UI.
"""
from bok_choy.web_app_test import WebAppTest
from ..edxapp_pages.studio.auto_auth import AutoAuthPage
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.open_response import OpenResponsePage
from ..fixtures.course import XBlockFixtureDesc, CourseFixture
from .helpers import load_data_str
class OpenResponseTest(WebAppTest):
"""
Tests that interact with ORA (Open Response Assessment) through the LMS UI.
"""
def setUp(self):
"""
Always start in the subsection with open response problems.
"""
# Create a unique course ID
self.course_info = {
'org': 'test_org',
'number': self.unique_id,
'run': 'test_run',
'display_name': 'Test Course' + self.unique_id
}
# Ensure fixtures are installed
super(OpenResponseTest, self).setUp()
# Log in and navigate to the essay problems
course_id = '{org}/{number}/{run}'.format(**self.course_info)
self.ui.visit('studio.auto_auth', course_id=course_id)
self.ui.visit('lms.course_info', course_id=course_id)
self.ui['lms.tab_nav'].go_to_tab('Courseware')
self.ui['lms.course_nav'].go_to_section(
'Example Week 2: Get Interactive', 'Homework - Essays'
)
@property
def page_object_classes(self):
return [AutoAuthPage, CourseInfoPage, TabNavPage, CourseNavPage, OpenResponsePage]
@property
def fixtures(self):
"""
Create a test course with open response problems.
"""
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('combinedopenended', 'Self-Assessed', data=load_data_str('ora_self_problem.xml')),
XBlockFixtureDesc('combinedopenended', 'AI-Assessed', data=load_data_str('ora_ai_problem.xml'))
)
)
)
return [course_fix]
def test_self_assessment(self):
"""
Test that the user can self-assess an essay.
"""
# Navigate to the self-assessment problem and submit an essay
self.ui['lms.course_nav'].go_to_sequential('Self-Assessed')
self._submit_essay('self', 'Censorship in the Libraries')
# Check the rubric categories
self.assertEqual(
self.ui['lms.open_response'].rubric_categories,
["Writing Applications", "Language Conventions"]
)
# Fill in the self-assessment rubric
self.ui['lms.open_response'].submit_self_assessment([0, 1])
# Expect that we get feedback
self.assertEqual(
self.ui['lms.open_response'].rubric_feedback,
['incorrect', 'correct']
)
def test_ai_assessment(self):
"""
Test that a user can submit an essay and receive AI feedback.
"""
# Navigate to the AI-assessment problem and submit an essay
self.ui['lms.course_nav'].go_to_sequential('AI-Assessed')
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."
)
def _submit_essay(self, expected_assessment_type, expected_prompt):
"""
Submit an essay and verify that the problem uses
the `expected_assessment_type` ("self", "ai", or "peer") and
shows the `expected_prompt` (a string).
"""
# Check the assessment type and prompt
self.assertEqual(self.ui['lms.open_response'].assessment_type, expected_assessment_type)
self.assertIn(expected_prompt, self.ui['lms.open_response'].prompt)
# Enter a response
essay = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut vehicula."
self.ui['lms.open_response'].set_response(essay)
# Save the response and expect some UI feedback
self.ui['lms.open_response'].save_response()
self.assertEqual(
self.ui['lms.open_response'].alert_message,
"Answer saved, but not yet submitted."
)
# Submit the response
self.ui['lms.open_response'].submit_response()
"""
Acceptance tests for Studio.
"""
from bok_choy.web_app_test import WebAppTest
from ..edxapp_pages.studio.asset_index import AssetIndexPage
from ..edxapp_pages.studio.auto_auth import AutoAuthPage
from ..edxapp_pages.studio.checklists import ChecklistsPage
from ..edxapp_pages.studio.course_import import ImportPage
from ..edxapp_pages.studio.course_info import CourseUpdatesPage
from ..edxapp_pages.studio.edit_tabs import StaticPagesPage
from ..edxapp_pages.studio.export import ExportPage
from ..edxapp_pages.studio.howitworks import HowitworksPage
from ..edxapp_pages.studio.index import DashboardPage
from ..edxapp_pages.studio.login import LoginPage
from ..edxapp_pages.studio.manage_users import CourseTeamPage
from ..edxapp_pages.studio.overview import CourseOutlinePage
from ..edxapp_pages.studio.settings import SettingsPage
from ..edxapp_pages.studio.settings_advanced import AdvancedSettingsPage
from ..edxapp_pages.studio.settings_graders import GradingPage
from ..edxapp_pages.studio.signup import SignupPage
from ..edxapp_pages.studio.textbooks import TextbooksPage
from ..fixtures.course import CourseFixture
class LoggedOutTest(WebAppTest):
"""
Smoke test for pages in Studio that are visible when logged out.
"""
@property
def page_object_classes(self):
return [LoginPage, HowitworksPage, SignupPage]
def test_page_existence(self):
"""
Make sure that all the pages are accessible.
Rather than fire up the browser just to check each url,
do them all sequentially in this testcase.
"""
for page in ['login', 'howitworks', 'signup']:
self.ui.visit('studio.{0}'.format(page))
class LoggedInPagesTest(WebAppTest):
"""
Tests that verify the pages in Studio that you can get to when logged
in and do not have a course yet.
"""
@property
def page_object_classes(self):
return [AutoAuthPage, DashboardPage]
def test_dashboard_no_courses(self):
"""
Make sure that you can get to the dashboard page without a course.
"""
self.ui.visit('studio.auto_auth', staff=True)
self.ui.visit('studio.dashboard')
class CoursePagesTest(WebAppTest):
"""
Tests that verify the pages in Studio that you can get to when logged
in and have a course.
"""
def setUp(self):
"""
Create a unique identifier for the course used in this test.
"""
# Define a unique course identifier
self.course_info = {
'org': 'test_org',
'number': '101',
'run': 'test_' + self.unique_id,
'display_name': 'Test Course ' + self.unique_id
}
# Ensure that the superclass sets up
super(CoursePagesTest, self).setUp()
@property
def page_object_classes(self):
return [
AutoAuthPage, AssetIndexPage, ChecklistsPage, ImportPage, CourseUpdatesPage,
StaticPagesPage, ExportPage, CourseTeamPage, CourseOutlinePage,
SettingsPage, AdvancedSettingsPage, GradingPage, TextbooksPage
]
@property
def fixtures(self):
super_fixtures = super(CoursePagesTest, self).fixtures
course_fix = CourseFixture(
self.course_info['org'],
self.course_info['number'],
self.course_info['run'],
self.course_info['display_name']
)
return set(super_fixtures + [course_fix])
def test_page_existence(self):
"""
Make sure that all these pages are accessible once you have a course.
Rather than fire up the browser just to check each url,
do them all sequentially in this testcase.
"""
pages = [
'uploads', 'checklists', 'import', 'updates', 'tabs', 'export',
'team', 'outline', 'settings', 'advanced', 'grading', 'textbooks'
]
# Log in
self.ui.visit('studio.auto_auth', staff=True)
course_id = '{org}.{number}.{run}'.format(**self.course_info)
for page in pages:
self.ui.visit('studio.{0}'.format(page), course_id=course_id)
from bok_choy.page_object import PageObject
from ..lms import BASE_URL
class InfoPage(PageObject):
"""
Info pages for the main site.
These are basically static pages, so we use one page
object to represent them all.
"""
# Dictionary mapping section names to URL paths
SECTION_PATH = {
'about': '/about',
'faq': '/faq',
'press': '/press',
'contact': '/contact',
'terms': '/tos',
'privacy': '/privacy',
'honor': '/honor',
}
# Dictionary mapping URLs to expected css selector
EXPECTED_CSS = {
'/about': 'section.vision',
'/faq': 'section.faq',
'/press': 'section.press',
'/contact': 'section.contact',
'/tos': 'section.tos',
'/privacy': 'section.privacy-policy',
'/honor': 'section.honor-code',
}
@property
def name(self):
return "lms.info"
@property
def requirejs(self):
return []
@property
def js_globals(self):
return []
def url(self, section=None):
return BASE_URL + self.SECTION_PATH[section]
def is_browser_on_page(self):
# Find the appropriate css based on the URL
for url_path, css_sel in self.EXPECTED_CSS.iteritems():
if self.browser.url.endswith(url_path):
return self.is_css_present(css_sel)
# Could not find the CSS based on the URL
return False
@classmethod
def sections(cls):
return cls.SECTION_PATH.keys()
from bok_choy.page_object import PageObject
from ..studio import BASE_URL
class HowitworksPage(PageObject):
"""
Home page for Studio when not logged in.
"""
@property
def name(self):
return "studio.howitworks"
@property
def requirejs(self):
return []
@property
def js_globals(self):
return []
def url(self):
return BASE_URL + "/howitworks"
def is_browser_on_page(self):
return self.browser.title == 'Welcome | edX Studio'
"""
Very simple test case to verify bok-choy integration.
"""
from bok_choy.web_app_test import WebAppTest
from edxapp_pages.lms.info import InfoPage
class InfoPageTest(WebAppTest):
"""
Test that the top-level pages in the LMS load.
"""
@property
def page_object_classes(self):
return [InfoPage]
def test_info(self):
for section_name in InfoPage.sections():
self.ui.visit('lms.info', section=section_name)
# Settings for bok choy tests
"""
Settings for bok choy tests
"""
import os
from path import path
CONFIG_ROOT = path(__file__).abspath().dirname()
CONFIG_ROOT = path(__file__).abspath().dirname() #pylint: disable=E1120
TEST_ROOT = CONFIG_ROOT.dirname().dirname() / "test_root"
########################## Prod-like settings ###################################
......@@ -16,7 +18,7 @@ TEST_ROOT = CONFIG_ROOT.dirname().dirname() / "test_root"
os.environ['SERVICE_VARIANT'] = 'bok_choy'
os.environ['CONFIG_ROOT'] = CONFIG_ROOT
from aws import * # pylint: disable=W0401, W0614
from .aws import * # pylint: disable=W0401, W0614
######################### Testing overrides ####################################
......@@ -45,7 +47,8 @@ import logging
LOG_OVERRIDES = [
('track.middleware', logging.CRITICAL),
('edxmako.shortcuts', logging.ERROR),
('dd.dogapi', logging.ERROR)
('dd.dogapi', logging.ERROR),
('edx.discussion', logging.CRITICAL),
]
for log_name, log_level in LOG_OVERRIDES:
logging.getLogger(log_name).setLevel(log_level)
......
......@@ -12,8 +12,7 @@ BOK_CHOY_NUM_PARALLEL = ENV.fetch('NUM_PARALLEL', 1).to_i
BOK_CHOY_TEST_TIMEOUT = ENV.fetch("TEST_TIMEOUT", 300).to_f
# Ensure that we have a directory to put logs and reports
BOK_CHOY_DIR = File.join(REPO_ROOT, "common", "test", "bok_choy")
BOK_CHOY_TEST_DIR = File.join(BOK_CHOY_DIR, "tests")
BOK_CHOY_TEST_DIR = File.join(REPO_ROOT, "common", "test", "acceptance", "tests")
BOK_CHOY_LOG_DIR = File.join(REPO_ROOT, "test_root", "log")
directory BOK_CHOY_LOG_DIR
......@@ -76,7 +75,7 @@ end
def nose_cmd(test_spec)
cmd = ["PYTHONPATH=#{BOK_CHOY_DIR}:$PYTHONPATH", "SCREENSHOT_DIR=#{BOK_CHOY_LOG_DIR}", "nosetests", test_spec]
cmd = ["SCREENSHOT_DIR='#{BOK_CHOY_LOG_DIR}'", "nosetests", test_spec]
if BOK_CHOY_NUM_PARALLEL > 1
cmd += ["--processes=#{BOK_CHOY_NUM_PARALLEL}", "--process-timeout=#{BOK_CHOY_TEST_TIMEOUT}"]
end
......
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