diff --git a/cms/envs/bok_choy.py b/cms/envs/bok_choy.py index 78573e83bd..1decc4fc87 100644 --- a/cms/envs/bok_choy.py +++ b/cms/envs/bok_choy.py @@ -1,4 +1,6 @@ -# 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 diff --git a/common/djangoapps/student/tests/test_auto_auth.py b/common/djangoapps/student/tests/test_auto_auth.py index 10936cff3f..63c575fdf4 100644 --- a/common/djangoapps/student/tests/test_auto_auth.py +++ b/common/djangoapps/student/tests/test_auto_auth.py @@ -1,6 +1,7 @@ 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): + # 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): + + # Create a staff user + self._auto_auth(username='test', staff='true') + user = User.objects.get(username='test') + self.assertTrue(user.is_staff) + + # Revoke staff privileges + self._auto_auth(username='test', staff='false') + user = User.objects.get(username='test') + self.assertFalse(user.is_staff) + + def test_course_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) + + # 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 to make sure multiple users are created. + Make a request to the auto-auth end-point and check + that the response is successful. """ - randint.return_value = 1 - self.client.get(self.url) + response = self.client.get(self.url, params) + self.assertEqual(response.status_code, 200) - randint.return_value = 2 - self.client.get(self.url) - - qset = User.objects.all() - - # 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') - - @patch.dict("django.conf.settings.FEATURES", {"MAX_AUTO_AUTH_USERS": 1}) - def test_login_already_created_user(self): - """ - Test that when we have reached the limit for automatic users - a subsequent request results in an already existant one being - logged in. - """ - # 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() - - # 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) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index aaf4ca8274..a77eff2847 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -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 + Create or configure a user account, then log in as that user. + + Enabled only when settings.FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] is true. + + 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` + + If username, email, or password are not provided, use + randomly generated credentials. """ - 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', } - - # generate random user credentials from a small name space (determined by settings) - name_base = 'USER_' - pass_base = 'PASS_' - - max_users = settings.FEATURES.get('MAX_AUTO_AUTH_USERS', 200) - number = random.randint(1, max_users) - - # 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) - # if they already are a user, log in - try: + # 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', + } + + # 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() - # 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() + # Set the user's global staff bit + if is_staff is not None: + user.is_staff = (is_staff == "true") + user.save() - # return empty success - return HttpResponse('') + # Activate the user + reg = Registration.objects.get(user=user) + reg.activate() + reg.save() + + # 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 diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index 0008412390..274530a965 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -1,6 +1,7 @@ # 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 diff --git a/common/test/bok_choy/tests/__init__.py b/common/test/acceptance/__init__.py similarity index 100% rename from common/test/bok_choy/tests/__init__.py rename to common/test/acceptance/__init__.py diff --git a/common/test/acceptance/edxapp_pages/__init__.py b/common/test/acceptance/edxapp_pages/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/test/bok_choy/edxapp_pages/lms/__init__.py b/common/test/acceptance/edxapp_pages/lms/__init__.py similarity index 100% rename from common/test/bok_choy/edxapp_pages/lms/__init__.py rename to common/test/acceptance/edxapp_pages/lms/__init__.py diff --git a/common/test/bok_choy/edxapp_pages/lms/course_about.py b/common/test/acceptance/edxapp_pages/lms/course_about.py similarity index 61% rename from common/test/bok_choy/edxapp_pages/lms/course_about.py rename to common/test/acceptance/edxapp_pages/lms/course_about.py index bd7c8a2dd2..db88513b2e 100644 --- a/common/test/bok_choy/edxapp_pages/lms/course_about.py +++ b/common/test/acceptance/edxapp_pages/lms/course_about.py @@ -1,34 +1,27 @@ +""" +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') diff --git a/common/test/bok_choy/edxapp_pages/lms/course_info.py b/common/test/acceptance/edxapp_pages/lms/course_info.py similarity index 75% rename from common/test/bok_choy/edxapp_pages/lms/course_info.py rename to common/test/acceptance/edxapp_pages/lms/course_info.py index ef1fd7b4cc..97ffe5a6f3 100644 --- a/common/test/bok_choy/edxapp_pages/lms/course_info.py +++ b/common/test/acceptance/edxapp_pages/lms/course_info.py @@ -1,5 +1,9 @@ +""" +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" + name = "lms.course_info" - @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 """ Go directly to the course info page for `course_id`. (e.g. "edX/Open_DemoX/edx_demo_course") diff --git a/common/test/bok_choy/edxapp_pages/lms/course_nav.py b/common/test/acceptance/edxapp_pages/lms/course_nav.py similarity index 92% rename from common/test/bok_choy/edxapp_pages/lms/course_nav.py rename to common/test/acceptance/edxapp_pages/lms/course_nav.py index c0ffa5f891..b8d6c8b023 100644 --- a/common/test/bok_choy/edxapp_pages/lms/course_nav.py +++ b/common/test/acceptance/edxapp_pages/lms/course_nav.py @@ -1,6 +1,10 @@ +""" +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'') + + 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] diff --git a/common/test/bok_choy/edxapp_pages/lms/dashboard.py b/common/test/acceptance/edxapp_pages/lms/dashboard.py similarity index 87% rename from common/test/bok_choy/edxapp_pages/lms/dashboard.py rename to common/test/acceptance/edxapp_pages/lms/dashboard.py index 6c7f932216..acf2556ac1 100644 --- a/common/test/bok_choy/edxapp_pages/lms/dashboard.py +++ b/common/test/acceptance/edxapp_pages/lms/dashboard.py @@ -1,5 +1,9 @@ +""" +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']) diff --git a/common/test/bok_choy/edxapp_pages/lms/find_courses.py b/common/test/acceptance/edxapp_pages/lms/find_courses.py similarity index 86% rename from common/test/bok_choy/edxapp_pages/lms/find_courses.py rename to common/test/acceptance/edxapp_pages/lms/find_courses.py index cdf7c0ba36..55fb883787 100644 --- a/common/test/bok_choy/edxapp_pages/lms/find_courses.py +++ b/common/test/acceptance/edxapp_pages/lms/find_courses.py @@ -1,6 +1,10 @@ +""" +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 diff --git a/common/test/bok_choy/edxapp_pages/lms/login.py b/common/test/acceptance/edxapp_pages/lms/login.py similarity index 81% rename from common/test/bok_choy/edxapp_pages/lms/login.py rename to common/test/acceptance/edxapp_pages/lms/login.py index 20bb115853..5012b32935 100644 --- a/common/test/bok_choy/edxapp_pages/lms/login.py +++ b/common/test/acceptance/edxapp_pages/lms/login.py @@ -1,6 +1,10 @@ +""" +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" diff --git a/common/test/bok_choy/edxapp_pages/lms/open_response.py b/common/test/acceptance/edxapp_pages/lms/open_response.py similarity index 92% rename from common/test/bok_choy/edxapp_pages/lms/open_response.py rename to common/test/acceptance/edxapp_pages/lms/open_response.py index 27ef3d58d2..712d5577d8 100644 --- a/common/test/bok_choy/edxapp_pages/lms/open_response.py +++ b/common/test/acceptance/edxapp_pages/lms/open_response.py @@ -1,3 +1,7 @@ +""" +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) diff --git a/common/test/bok_choy/edxapp_pages/lms/progress.py b/common/test/acceptance/edxapp_pages/lms/progress.py similarity index 90% rename from common/test/bok_choy/edxapp_pages/lms/progress.py rename to common/test/acceptance/edxapp_pages/lms/progress.py index 88c2e7b2ab..a0fe656e85 100644 --- a/common/test/bok_choy/edxapp_pages/lms/progress.py +++ b/common/test/acceptance/edxapp_pages/lms/progress.py @@ -1,5 +1,9 @@ +""" +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" + name = "lms.progress" - @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 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 diff --git a/common/test/bok_choy/edxapp_pages/lms/register.py b/common/test/acceptance/edxapp_pages/lms/register.py similarity index 79% rename from common/test/bok_choy/edxapp_pages/lms/register.py rename to common/test/acceptance/edxapp_pages/lms/register.py index 9d52c190eb..0e722b9657 100644 --- a/common/test/bok_choy/edxapp_pages/lms/register.py +++ b/common/test/acceptance/edxapp_pages/lms/register.py @@ -1,5 +1,9 @@ +""" +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" + name = "lms.register" - @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 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" diff --git a/common/test/bok_choy/edxapp_pages/lms/tab_nav.py b/common/test/acceptance/edxapp_pages/lms/tab_nav.py similarity index 90% rename from common/test/bok_choy/edxapp_pages/lms/tab_nav.py rename to common/test/acceptance/edxapp_pages/lms/tab_nav.py index 3ec8c15141..9b4b4ccd11 100644 --- a/common/test/bok_choy/edxapp_pages/lms/tab_nav.py +++ b/common/test/acceptance/edxapp_pages/lms/tab_nav.py @@ -1,6 +1,9 @@ +""" +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') diff --git a/common/test/bok_choy/edxapp_pages/lms/video.py b/common/test/acceptance/edxapp_pages/lms/video.py similarity index 91% rename from common/test/bok_choy/edxapp_pages/lms/video.py rename to common/test/acceptance/edxapp_pages/lms/video.py index 142da8b608..865664a61b 100644 --- a/common/test/bok_choy/edxapp_pages/lms/video.py +++ b/common/test/acceptance/edxapp_pages/lms/video.py @@ -1,8 +1,10 @@ -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') diff --git a/common/test/bok_choy/edxapp_pages/studio/__init__.py b/common/test/acceptance/edxapp_pages/studio/__init__.py similarity index 100% rename from common/test/bok_choy/edxapp_pages/studio/__init__.py rename to common/test/acceptance/edxapp_pages/studio/__init__.py diff --git a/common/test/acceptance/edxapp_pages/studio/asset_index.py b/common/test/acceptance/edxapp_pages/studio/asset_index.py new file mode 100644 index 0000000000..4e20bb12b7 --- /dev/null +++ b/common/test/acceptance/edxapp_pages/studio/asset_index.py @@ -0,0 +1,29 @@ +""" +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') diff --git a/common/test/acceptance/edxapp_pages/studio/auto_auth.py b/common/test/acceptance/edxapp_pages/studio/auto_auth.py new file mode 100644 index 0000000000..29df6fc632 --- /dev/null +++ b/common/test/acceptance/edxapp_pages/studio/auto_auth.py @@ -0,0 +1,63 @@ +""" +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 diff --git a/common/test/acceptance/edxapp_pages/studio/checklists.py b/common/test/acceptance/edxapp_pages/studio/checklists.py new file mode 100644 index 0000000000..981f258a4d --- /dev/null +++ b/common/test/acceptance/edxapp_pages/studio/checklists.py @@ -0,0 +1,29 @@ +""" +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') diff --git a/common/test/acceptance/edxapp_pages/studio/course_import.py b/common/test/acceptance/edxapp_pages/studio/course_import.py new file mode 100644 index 0000000000..504594cd59 --- /dev/null +++ b/common/test/acceptance/edxapp_pages/studio/course_import.py @@ -0,0 +1,29 @@ +""" +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') diff --git a/common/test/acceptance/edxapp_pages/studio/course_info.py b/common/test/acceptance/edxapp_pages/studio/course_info.py new file mode 100644 index 0000000000..7baa4425b2 --- /dev/null +++ b/common/test/acceptance/edxapp_pages/studio/course_info.py @@ -0,0 +1,29 @@ +""" +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') diff --git a/common/test/acceptance/edxapp_pages/studio/edit_subsection.py b/common/test/acceptance/edxapp_pages/studio/edit_subsection.py new file mode 100644 index 0000000000..32ac69236f --- /dev/null +++ b/common/test/acceptance/edxapp_pages/studio/edit_subsection.py @@ -0,0 +1,19 @@ +""" +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') diff --git a/common/test/acceptance/edxapp_pages/studio/edit_tabs.py b/common/test/acceptance/edxapp_pages/studio/edit_tabs.py new file mode 100644 index 0000000000..3339374c1c --- /dev/null +++ b/common/test/acceptance/edxapp_pages/studio/edit_tabs.py @@ -0,0 +1,29 @@ +""" +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') diff --git a/common/test/acceptance/edxapp_pages/studio/export.py b/common/test/acceptance/edxapp_pages/studio/export.py new file mode 100644 index 0000000000..3de953576d --- /dev/null +++ b/common/test/acceptance/edxapp_pages/studio/export.py @@ -0,0 +1,29 @@ +""" +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') diff --git a/common/test/acceptance/edxapp_pages/studio/helpers.py b/common/test/acceptance/edxapp_pages/studio/helpers.py new file mode 100644 index 0000000000..ae64e889c8 --- /dev/null +++ b/common/test/acceptance/edxapp_pages/studio/helpers.py @@ -0,0 +1,28 @@ +""" +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) diff --git a/common/test/acceptance/edxapp_pages/studio/howitworks.py b/common/test/acceptance/edxapp_pages/studio/howitworks.py new file mode 100644 index 0000000000..0b17100030 --- /dev/null +++ b/common/test/acceptance/edxapp_pages/studio/howitworks.py @@ -0,0 +1,20 @@ +""" +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') diff --git a/common/test/acceptance/edxapp_pages/studio/index.py b/common/test/acceptance/edxapp_pages/studio/index.py new file mode 100644 index 0000000000..f98271fe77 --- /dev/null +++ b/common/test/acceptance/edxapp_pages/studio/index.py @@ -0,0 +1,20 @@ +""" +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') diff --git a/common/test/acceptance/edxapp_pages/studio/login.py b/common/test/acceptance/edxapp_pages/studio/login.py new file mode 100644 index 0000000000..946a918cd8 --- /dev/null +++ b/common/test/acceptance/edxapp_pages/studio/login.py @@ -0,0 +1,37 @@ +""" +Login page for Studio. +""" + +from bok_choy.page_object import PageObject +from bok_choy.promise import EmptyPromise, fulfill_after +from . import BASE_URL + + +class LoginPage(PageObject): + """ + Login page for Studio. + """ + + name = "studio.login" + + def url(self): + return BASE_URL + "/signin" + + def is_browser_on_page(self): + return self.is_css_present('body.view-signin') + + def login(self, email, password): + """ + Attempt to log in using `email` and `password`. + """ + + # 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') diff --git a/common/test/acceptance/edxapp_pages/studio/manage_users.py b/common/test/acceptance/edxapp_pages/studio/manage_users.py new file mode 100644 index 0000000000..8c253532fc --- /dev/null +++ b/common/test/acceptance/edxapp_pages/studio/manage_users.py @@ -0,0 +1,29 @@ +""" +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') diff --git a/common/test/acceptance/edxapp_pages/studio/overview.py b/common/test/acceptance/edxapp_pages/studio/overview.py new file mode 100644 index 0000000000..f94d232d36 --- /dev/null +++ b/common/test/acceptance/edxapp_pages/studio/overview.py @@ -0,0 +1,29 @@ +""" +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') diff --git a/common/test/acceptance/edxapp_pages/studio/settings.py b/common/test/acceptance/edxapp_pages/studio/settings.py new file mode 100644 index 0000000000..f24c89c2e9 --- /dev/null +++ b/common/test/acceptance/edxapp_pages/studio/settings.py @@ -0,0 +1,29 @@ +""" +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') diff --git a/common/test/acceptance/edxapp_pages/studio/settings_advanced.py b/common/test/acceptance/edxapp_pages/studio/settings_advanced.py new file mode 100644 index 0000000000..916ec70def --- /dev/null +++ b/common/test/acceptance/edxapp_pages/studio/settings_advanced.py @@ -0,0 +1,29 @@ +""" +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') diff --git a/common/test/acceptance/edxapp_pages/studio/settings_graders.py b/common/test/acceptance/edxapp_pages/studio/settings_graders.py new file mode 100644 index 0000000000..24c9623d77 --- /dev/null +++ b/common/test/acceptance/edxapp_pages/studio/settings_graders.py @@ -0,0 +1,29 @@ +""" +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') diff --git a/common/test/acceptance/edxapp_pages/studio/signup.py b/common/test/acceptance/edxapp_pages/studio/signup.py new file mode 100644 index 0000000000..8847fb8416 --- /dev/null +++ b/common/test/acceptance/edxapp_pages/studio/signup.py @@ -0,0 +1,16 @@ +from bok_choy.page_object import PageObject +from . import BASE_URL + + +class SignupPage(PageObject): + """ + Signup page for Studio. + """ + + name = "studio.signup" + + def url(self): + return BASE_URL + "/signup" + + def is_browser_on_page(self): + return self.is_css_present('body.view-signup') diff --git a/common/test/acceptance/edxapp_pages/studio/textbooks.py b/common/test/acceptance/edxapp_pages/studio/textbooks.py new file mode 100644 index 0000000000..0c0dd579e8 --- /dev/null +++ b/common/test/acceptance/edxapp_pages/studio/textbooks.py @@ -0,0 +1,29 @@ +""" +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') diff --git a/common/test/acceptance/edxapp_pages/studio/unit.py b/common/test/acceptance/edxapp_pages/studio/unit.py new file mode 100644 index 0000000000..2a9294c9f7 --- /dev/null +++ b/common/test/acceptance/edxapp_pages/studio/unit.py @@ -0,0 +1,19 @@ +""" +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') diff --git a/common/test/acceptance/fixtures/__init__.py b/common/test/acceptance/fixtures/__init__.py new file mode 100644 index 0000000000..0f6c1937f4 --- /dev/null +++ b/common/test/acceptance/fixtures/__init__.py @@ -0,0 +1,4 @@ +import os + +# Get the URL of the instance under test +STUDIO_BASE_URL = os.environ.get('studio_url', 'http://localhost:8031') diff --git a/common/test/acceptance/fixtures/base.py b/common/test/acceptance/fixtures/base.py new file mode 100644 index 0000000000..6ee4fb07e7 --- /dev/null +++ b/common/test/acceptance/fixtures/base.py @@ -0,0 +1,30 @@ +""" +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) diff --git a/common/test/acceptance/fixtures/course.py b/common/test/acceptance/fixtures/course.py new file mode 100644 index 0000000000..54af80aacb --- /dev/null +++ b/common/test/acceptance/fixtures/course.py @@ -0,0 +1,321 @@ +""" +Fixture to create a course and course components (XBlocks). +""" + +import json +import datetime +from textwrap import dedent +import requests +from lazy import lazy +from bok_choy.web_app_fixture import WebAppFixture, WebAppFixtureError +from . import STUDIO_BASE_URL + + +class StudioApiFixture(WebAppFixture): + """ + Base class for fixtures that use the Studio restful API. + """ + + @lazy + def session_cookies(self): + """ + Log in as a staff user, then return the cookies for the session (as a dict) + Raises a `WebAppFixtureError` if the login fails. + """ + + # Use auto-auth to retrieve session cookies for a logged in user + response = requests.get(STUDIO_BASE_URL + "/auto_auth?staff=true") + + # Return the cookies from the request + if response.ok: + return {key: val for key, val in response.cookies.items()} + + else: + msg = "Could not log in to use Studio restful API. Status code: {0}".format(response.status_code) + raise WebAppFixtureError(msg) + + @lazy + def headers(self): + """ + Default HTTP headers dict. + """ + return { + 'Content-type': 'application/json', + 'Accept': 'application/json', + 'X-CSRFToken': self.session_cookies.get('csrftoken', '') + } + + +class XBlockFixtureDesc(object): + """ + Description of an XBlock, used to configure a course fixture. + """ + + def __init__(self, category, display_name, data=None, metadata=None, grader_type=None, publish='make_public'): + """ + Configure the XBlock to be created by the fixture. + These arguments have the same meaning as in the Studio REST API: + * `category` + * `display_name` + * `data` + * `metadata` + * `grader_type` + * `publish` + """ + self.category = category + self.display_name = display_name + self.data = data + self.metadata = metadata + self.grader_type = grader_type + self.publish = publish + self.children = [] + + def add_children(self, *args): + """ + Add child XBlocks to this XBlock. + Each item in `args` is an `XBlockFixtureDescriptor` object. + + Returns the `xblock_desc` instance to allow chaining. + """ + self.children.extend(args) + return self + + def serialize(self, parent_loc=None): + """ + Return a JSON representation of the XBlock, suitable + for sending as POST data to /xblock + + XBlocks are always set to public visibility. + """ + payload = { + 'category': self.category, + 'display_name': self.display_name, + 'data': self.data, + 'metadata': self.metadata, + 'grader_type': self.grader_type, + 'publish': self.publish + } + + if parent_loc is not None: + payload['parent_locator'] = parent_loc + + return json.dumps(payload) + + def __str__(self): + """ + Return a string representation of the description. + Useful for error messages. + """ + return dedent(""" + + """).strip().format( + self.category, self.data, self.metadata, + self.grader_type, self.publish, self.children + ) + + +class CourseFixture(StudioApiFixture): + """ + Fixture for ensuring that a course exists. + + WARNING: This fixture is NOT idempotent. To avoid conflicts + between tests, you should use unique course identifiers for each fixture. + """ + + def __init__(self, org, number, run, display_name, start_date=None, end_date=None): + """ + Configure the course fixture to create a course with + + `org`, `number`, `run`, and `display_name` (all unicode). + + `start_date` and `end_date` are datetime objects indicating the course start and end date. + The default is for the course to have started in the distant past, which is generally what + we want for testing so students can enroll. + + These have the same meaning as in the Studio restful API /course end-point. + """ + self._course_dict = { + 'org': org, + 'number': number, + 'run': run, + 'display_name': display_name + } + + # Set a default start date to the past, but use Studio's + # default for the end date (meaning we don't set it here) + if start_date is None: + start_date = datetime.datetime(1970, 1, 1) + + self._course_details = { + 'start_date': start_date.isoformat(), + } + + if end_date is not None: + self._course_details['end_date'] = end_date.isoformat() + + self._children = [] + + def __str__(self): + """ + String representation of the course fixture, useful for debugging. + """ + return "".format(**self._course_dict) + + def add_children(self, *args): + """ + Add children XBlock to the course. + Each item in `args` is an `XBlockFixtureDescriptor` object. + + Returns the course fixture to allow chaining. + """ + self._children.extend(args) + return self + + def install(self): + """ + Create the course and XBlocks within the course. + This is NOT an idempotent method; if the course already exists, this will + raise a `WebAppFixtureError`. You should use unique course identifiers to avoid + conflicts between tests. + """ + self._create_course() + self._configure_course() + self._create_xblock_children(self._course_loc, self._children) + + @property + def _course_loc(self): + """ + Return the locator string for the course. + """ + return "{org}.{number}.{run}/branch/draft/block/{run}".format(**self._course_dict) + + def _create_course(self): + """ + Create the course described in the fixture. + """ + # If the course already exists, this will respond + # with a 200 and an error message, which we ignore. + response = requests.post( + STUDIO_BASE_URL + '/course', + data=self._encode_post_dict(self._course_dict), + headers=self.headers, + cookies=self.session_cookies + ) + + try: + err = response.json().get('ErrMsg') + + except ValueError: + raise WebAppFixtureError( + "Could not parse response from course request as JSON: '{0}'".format( + response.content)) + + # This will occur if the course identifier is not unique + if err is not None: + raise WebAppFixtureError("Could not create course {0}. Error message: '{1}'".format(self, err)) + + if not response.ok: + raise WebAppFixtureError( + "Could not create course {0}. Status was {1}".format( + self._course_dict, response.status_code)) + + def _configure_course(self): + """ + Configure course settings (e.g. start and end date) + """ + url = STUDIO_BASE_URL + '/settings/details/' + self._course_loc + + # First, get the current values + response = requests.get(url, headers=self.headers, cookies=self.session_cookies) + + if not response.ok: + raise WebAppFixtureError( + "Could not retrieve course details. Status was {0}".format( + response.status_code)) + + try: + details = response.json() + except ValueError: + raise WebAppFixtureError( + "Could not decode course details as JSON: '{0}'".format(old_details) + ) + + # Update the old details with our overrides + details.update(self._course_details) + + # POST the updated details to Studio + response = requests.post( + url, data=self._encode_post_dict(details), + headers=self.headers, + cookies=self.session_cookies + ) + + if not response.ok: + raise WebAppFixtureError( + "Could not update course details to '{0}'. Status was {1}.".format( + self._course_details, response.status_code)) + + def _create_xblock_children(self, parent_loc, xblock_descriptions): + """ + Recursively create XBlock children. + """ + for desc in xblock_descriptions: + loc = self._create_xblock(parent_loc, desc) + self._create_xblock_children(loc, desc.children) + + def _create_xblock(self, parent_loc, xblock_desc): + """ + Create an XBlock with `parent_loc` (the location of the parent block) + and `xblock_desc` (an `XBlockFixtureDesc` instance). + """ + # Create the new XBlock + response = requests.post( + STUDIO_BASE_URL + '/xblock', + data=xblock_desc.serialize(parent_loc=parent_loc), + headers=self.headers, + cookies=self.session_cookies + ) + + if not response.ok: + msg = "Could not create {0}. Status was {1}".format(xblock_desc, response.status_code) + raise WebAppFixtureError(msg) + + try: + loc = response.json().get('locator') + + except ValueError: + raise WebAppFixtureError("Could not decode JSON from '{0}'".format(response.content)) + + if loc is not None: + + # Configure the XBlock + response = requests.post( + STUDIO_BASE_URL + '/xblock/' + loc, + data=xblock_desc.serialize(), + headers=self.headers, + cookies=self.session_cookies + ) + + if response.ok: + return loc + else: + raise WebAppFixtureError("Could not update {0}".format(xblock_desc)) + + else: + raise WebAppFixtureError("Could not retrieve location of {0}".format(xblock_desc)) + + def _encode_post_dict(self, post_dict): + """ + Encode `post_dict` (a dictionary) as UTF-8 encoded JSON. + """ + return json.dumps({ + k: v.encode('utf-8') if v is not None else v + for k, v in post_dict.items() + }) diff --git a/common/test/bok_choy/setup.py b/common/test/acceptance/setup.py similarity index 100% rename from common/test/bok_choy/setup.py rename to common/test/acceptance/setup.py diff --git a/common/test/acceptance/tests/__init__.py b/common/test/acceptance/tests/__init__.py new file mode 100644 index 0000000000..011a8d5140 --- /dev/null +++ b/common/test/acceptance/tests/__init__.py @@ -0,0 +1,10 @@ +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) diff --git a/common/test/acceptance/tests/data/ora_ai_problem.xml b/common/test/acceptance/tests/data/ora_ai_problem.xml new file mode 100644 index 0000000000..2b11831561 --- /dev/null +++ b/common/test/acceptance/tests/data/ora_ai_problem.xml @@ -0,0 +1,30 @@ + + + + + Writing Applications + + + + + Language Conventions + + + + + + +

Censorship in the Libraries

+

"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

+

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.

+
+ + + + Enter essay here. + This is the answer. + {"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"} + + + +
diff --git a/common/test/acceptance/tests/data/ora_self_problem.xml b/common/test/acceptance/tests/data/ora_self_problem.xml new file mode 100644 index 0000000000..b76f90ce63 --- /dev/null +++ b/common/test/acceptance/tests/data/ora_self_problem.xml @@ -0,0 +1,24 @@ + + + + + Writing Applications + + + + + Language Conventions + + + + + + +

Censorship in the Libraries

+

"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

+

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.

+
+ + + +
diff --git a/common/test/acceptance/tests/helpers.py b/common/test/acceptance/tests/helpers.py new file mode 100644 index 0000000000..ac3278f494 --- /dev/null +++ b/common/test/acceptance/tests/helpers.py @@ -0,0 +1,14 @@ +""" +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() diff --git a/common/test/acceptance/tests/test_ora.py b/common/test/acceptance/tests/test_ora.py new file mode 100644 index 0000000000..29660b3c16 --- /dev/null +++ b/common/test/acceptance/tests/test_ora.py @@ -0,0 +1,132 @@ +""" +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() diff --git a/common/test/acceptance/tests/test_studio.py b/common/test/acceptance/tests/test_studio.py new file mode 100644 index 0000000000..9149d894a6 --- /dev/null +++ b/common/test/acceptance/tests/test_studio.py @@ -0,0 +1,118 @@ +""" +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) diff --git a/common/test/bok_choy/edxapp_pages/__init__.py b/common/test/bok_choy/edxapp_pages/__init__.py deleted file mode 100644 index 8b13789179..0000000000 --- a/common/test/bok_choy/edxapp_pages/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/common/test/bok_choy/edxapp_pages/lms/info.py b/common/test/bok_choy/edxapp_pages/lms/info.py deleted file mode 100644 index 722519c700..0000000000 --- a/common/test/bok_choy/edxapp_pages/lms/info.py +++ /dev/null @@ -1,61 +0,0 @@ -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() diff --git a/common/test/bok_choy/edxapp_pages/studio/howitworks.py b/common/test/bok_choy/edxapp_pages/studio/howitworks.py deleted file mode 100644 index 2c3ef5649a..0000000000 --- a/common/test/bok_choy/edxapp_pages/studio/howitworks.py +++ /dev/null @@ -1,26 +0,0 @@ -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' diff --git a/common/test/bok_choy/edxapp_pages/studio/login.py b/common/test/bok_choy/edxapp_pages/studio/login.py deleted file mode 100644 index c265235b82..0000000000 --- a/common/test/bok_choy/edxapp_pages/studio/login.py +++ /dev/null @@ -1,34 +0,0 @@ -from bok_choy.page_object import PageObject -from ..studio import BASE_URL - - -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 [] - - def url(self): - return BASE_URL + "/signin" - - def is_browser_on_page(self): - return self.browser.title == 'Sign In | edX Studio' - - 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') diff --git a/common/test/bok_choy/edxapp_pages/studio/signup.py b/common/test/bok_choy/edxapp_pages/studio/signup.py deleted file mode 100644 index 3045770bab..0000000000 --- a/common/test/bok_choy/edxapp_pages/studio/signup.py +++ /dev/null @@ -1,26 +0,0 @@ -from bok_choy.page_object import PageObject -from ..studio import BASE_URL - - -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 [] - - def url(self): - return BASE_URL + "/signup" - - def is_browser_on_page(self): - return self.browser.title == 'Sign Up | edX Studio' diff --git a/common/test/bok_choy/tests/test_info_pages.py b/common/test/bok_choy/tests/test_info_pages.py deleted file mode 100644 index c7bceb1d53..0000000000 --- a/common/test/bok_choy/tests/test_info_pages.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -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) diff --git a/lms/envs/bok_choy.py b/lms/envs/bok_choy.py index 6674988e47..cd4fac9aee 100644 --- a/lms/envs/bok_choy.py +++ b/lms/envs/bok_choy.py @@ -1,10 +1,12 @@ -# 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) diff --git a/rakelib/bok_choy.rake b/rakelib/bok_choy.rake index b7e330f475..f13e5a1e59 100644 --- a/rakelib/bok_choy.rake +++ b/rakelib/bok_choy.rake @@ -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