diff --git a/cms/djangoapps/contentstore/features/__init__.py b/cms/djangoapps/contentstore/features/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/cms/djangoapps/contentstore/features/advanced_settings.py b/cms/djangoapps/contentstore/features/advanced_settings.py
deleted file mode 100644
index 63d63d3761..0000000000
--- a/cms/djangoapps/contentstore/features/advanced_settings.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# pylint: disable=missing-docstring
-# pylint: disable=redefined-outer-name
-
-from lettuce import world
-from cms.djangoapps.contentstore.features.common import press_the_notification_button, type_in_codemirror
-
-KEY_CSS = '.key h3.title'
-ADVANCED_MODULES_KEY = "Advanced Module List"
-
-
-def get_index_of(expected_key):
- for i, element in enumerate(world.css_find(KEY_CSS)):
- # Sometimes get stale reference if I hold on to the array of elements
- key = world.css_value(KEY_CSS, index=i)
- if key == expected_key:
- return i
-
- return -1
-
-
-def change_value(step, key, new_value):
- index = get_index_of(key)
- type_in_codemirror(index, new_value)
- press_the_notification_button(step, "Save")
- world.wait_for_ajax_complete()
diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py
deleted file mode 100644
index 845d0df56c..0000000000
--- a/cms/djangoapps/contentstore/features/common.py
+++ /dev/null
@@ -1,406 +0,0 @@
-# pylint: disable=missing-docstring
-# pylint: disable=no-member
-# pylint: disable=redefined-outer-name
-
-import os
-from logging import getLogger
-
-from django.conf import settings
-from lettuce import step, world
-from selenium.webdriver.common.keys import Keys
-
-from openedx.core.lib.tests.tools import assert_in # pylint: disable=no-name-in-module
-from student import auth
-from student.models import get_user
-from student.roles import CourseInstructorRole, CourseStaffRole, GlobalStaff
-from student.tests.factories import AdminFactory
-from terrain.browser import reset_data
-
-logger = getLogger(__name__)
-
-
-TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
-
-
-@step('I (?:visit|access|open) the Studio homepage$')
-def i_visit_the_studio_homepage(_step):
- # To make this go to port 8001, put
- # LETTUCE_SERVER_PORT = 8001
- # in your settings.py file.
- world.visit('/')
- signin_css = 'a.action-signin'
- assert world.is_css_present(signin_css)
-
-
-@step('I am logged into Studio$')
-def i_am_logged_into_studio(_step):
- log_into_studio()
-
-
-@step('I confirm the alert$')
-def i_confirm_with_ok(_step):
- world.browser.get_alert().accept()
-
-
-@step(u'I press the "([^"]*)" delete icon$')
-def i_press_the_category_delete_icon(_step, category):
- if category == 'section':
- css = 'a.action.delete-section-button'
- elif category == 'subsection':
- css = 'a.action.delete-subsection-button'
- else:
- assert False, u'Invalid category: %s' % category
- world.css_click(css)
-
-
-@step('I have opened a new course in Studio$')
-def i_have_opened_a_new_course(_step):
- open_new_course()
-
-
-@step('I have populated a new course in Studio$')
-def i_have_populated_a_new_course(_step):
- world.clear_courses()
- course = world.CourseFactory.create()
- world.scenario_dict['COURSE'] = course
- section = world.ItemFactory.create(parent_location=course.location)
- world.ItemFactory.create(
- parent_location=section.location,
- category='sequential',
- display_name='Subsection One',
- )
- user = create_studio_user(is_staff=False)
- add_course_author(user, course)
-
- log_into_studio()
-
- world.css_click('a.course-link')
- world.wait_for_js_to_load()
-
-
-@step('(I select|s?he selects) the new course')
-def select_new_course(_step, _whom):
- course_link_css = 'a.course-link'
- world.css_click(course_link_css)
-
-
-@step(u'I press the "([^"]*)" notification button$')
-def press_the_notification_button(_step, name):
-
- # Because the notification uses a CSS transition,
- # Selenium will always report it as being visible.
- # This makes it very difficult to successfully click
- # the "Save" button at the UI level.
- # Instead, we use JavaScript to reliably click
- # the button.
- btn_css = u'div#page-notification button.action-%s' % name.lower()
- world.trigger_event(btn_css, event='focus')
- world.browser.execute_script("$('{}').click()".format(btn_css))
- world.wait_for_ajax_complete()
-
-
-@step('I change the "(.*)" field to "(.*)"$')
-def i_change_field_to_value(_step, field, value):
- field_css = '#%s' % '-'.join([s.lower() for s in field.split()])
- ele = world.css_find(field_css).first
- ele.fill(value)
- ele._element.send_keys(Keys.ENTER) # pylint: disable=protected-access
-
-
-@step('I reset the database')
-def reset_the_db(_step):
- """
- When running Lettuce tests using examples (i.e. "Confirmation is
- shown on save" in course-settings.feature), the normal hooks
- aren't called between examples. reset_data should run before each
- scenario to flush the test database. When this doesn't happen we
- get errors due to trying to insert a non-unique entry. So instead,
- we delete the database manually. This has the effect of removing
- any users and courses that have been created during the test run.
- """
- reset_data(None)
-
-
-@step('I see a confirmation that my changes have been saved')
-def i_see_a_confirmation(_step):
- confirmation_css = '#alert-confirmation'
- assert world.is_css_present(confirmation_css)
-
-
-def open_new_course():
- world.clear_courses()
- create_studio_user()
- log_into_studio()
- create_a_course()
-
-
-def create_studio_user(
- uname='robot',
- email='robot+studio@edx.org',
- password='test',
- is_staff=False):
- studio_user = world.UserFactory(
- username=uname,
- email=email,
- password=password,
- is_staff=is_staff)
-
- registration = world.RegistrationFactory(user=studio_user)
- registration.register(studio_user)
- registration.activate()
-
- return studio_user
-
-
-def fill_in_course_info(
- name='Robot Super Course',
- org='MITx',
- num='101',
- run='2013_Spring'):
- world.css_fill('.new-course-name', name)
- world.css_fill('.new-course-org', org)
- world.css_fill('.new-course-number', num)
- world.css_fill('.new-course-run', run)
-
-
-def log_into_studio(
- uname='robot',
- email='robot+studio@edx.org',
- password='test',
- name='Robot Studio'):
-
- world.log_in(username=uname, password=password, email=email, name=name)
- # Navigate to the studio dashboard
- world.visit('/')
- assert_in(uname, world.css_text('span.account-username', timeout=10))
-
-
-def add_course_author(user, course):
- """
- Add the user to the instructor group of the course
- so they will have the permissions to see it in studio
- """
- global_admin = AdminFactory()
- for role in (CourseStaffRole, CourseInstructorRole):
- auth.add_users(global_admin, role(course.id), user)
-
-
-def create_a_course():
- course = world.CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
- world.scenario_dict['COURSE'] = course
-
- user = world.scenario_dict.get("USER")
- if not user:
- user = get_user('robot+studio@edx.org')
-
- add_course_author(user, course)
-
- # Navigate to the studio dashboard
- world.visit('/')
- course_link_css = 'a.course-link'
- world.css_click(course_link_css)
- course_title_css = 'span.course-title'
- assert world.is_css_present(course_title_css)
-
-
-def add_section():
- world.css_click('.outline .button-new')
- assert world.is_css_present('.outline-section .xblock-field-value')
-
-
-def set_date_and_time(date_css, desired_date, time_css, desired_time, key=None):
- set_element_value(date_css, desired_date, key)
- world.wait_for_ajax_complete()
-
- set_element_value(time_css, desired_time, key)
- world.wait_for_ajax_complete()
-
-
-def set_element_value(element_css, element_value, key=None):
- element = world.css_find(element_css).first
- element.fill(element_value)
- # hit TAB or provided key to trigger save content
- if key is not None:
- element._element.send_keys(getattr(Keys, key)) # pylint: disable=protected-access
- else:
- element._element.send_keys(Keys.TAB) # pylint: disable=protected-access
-
-
-@step('I have enabled the (.*) advanced module$')
-def i_enabled_the_advanced_module(step, module):
- step.given('I have opened a new course section in Studio')
- world.css_click('.nav-course-settings')
- world.css_click('.nav-course-settings-advanced a')
- type_in_codemirror(0, '["%s"]' % module)
- press_the_notification_button(step, 'Save')
-
-
-@world.absorb
-def create_unit_from_course_outline():
- """
- Expands the section and clicks on the New Unit link.
- The end result is the page where the user is editing the new unit.
- """
- css_selectors = [
- '.outline-subsection .expand-collapse', '.outline-subsection .button-new'
- ]
- for selector in css_selectors:
- world.css_click(selector)
-
- world.wait_for_mathjax()
- world.wait_for_loading()
-
- assert world.is_css_present('ul.new-component-type')
-
-
-@world.absorb
-def wait_for_loading():
- """
- Waits for the loading indicator to be hidden.
- """
- world.wait_for(lambda _driver: len(world.browser.find_by_css('div.ui-loading.is-hidden')) > 0)
-
-
-@step('I have clicked the new unit button$')
-@step(u'I am in Studio editing a new unit$')
-def edit_new_unit(step):
- step.given('I have populated a new course in Studio')
- create_unit_from_course_outline()
-
-
-@step('the save notification button is disabled')
-def save_button_disabled(_step):
- button_css = '.action-save'
- disabled = 'is-disabled'
- assert world.css_has_class(button_css, disabled)
-
-
-@step('the "([^"]*)" button is disabled')
-def button_disabled(_step, value):
- button_css = 'input[value="%s"]' % value
- assert world.css_has_class(button_css, 'is-disabled')
-
-
-def _do_studio_prompt_action(intent, action):
- """
- Wait for a studio prompt to appear and press the specified action button
- See common/js/components/views/feedback_prompt.js for implementation
- """
- assert intent in [
- 'warning',
- 'error',
- 'confirmation',
- 'announcement',
- 'step-required',
- 'help',
- 'mini',
- ]
- assert action in ['primary', 'secondary']
-
- world.wait_for_present('div.wrapper-prompt.is-shown#prompt-{}'.format(intent))
-
- action_css = u'li.nav-item > button.action-{}'.format(action)
- world.trigger_event(action_css, event='focus')
- world.browser.execute_script("$('{}').click()".format(action_css))
-
- world.wait_for_ajax_complete()
- world.wait_for_present('div.wrapper-prompt.is-hiding#prompt-{}'.format(intent))
-
-
-@world.absorb
-def confirm_studio_prompt():
- _do_studio_prompt_action('warning', 'primary')
-
-
-@step('I confirm the prompt')
-def confirm_the_prompt(_step):
- confirm_studio_prompt()
-
-
-@step(u'I am shown a prompt$')
-def i_am_shown_a_notification(_step):
- assert world.is_css_present('.wrapper-prompt')
-
-
-def type_in_codemirror(index, text, find_prefix="$"):
- script = """
- var cm = {find_prefix}('div.CodeMirror:eq({index})').get(0).CodeMirror;
- cm.getInputField().focus();
- cm.setValue(arguments[0]);
- cm.getInputField().blur();""".format(index=index, find_prefix=find_prefix)
- world.browser.driver.execute_script(script, str(text))
- world.wait_for_ajax_complete()
-
-
-def get_codemirror_value(index=0, find_prefix="$"):
- return world.browser.driver.execute_script(
- u"""
- return {find_prefix}('div.CodeMirror:eq({index})').get(0).CodeMirror.getValue();
- """.format(index=index, find_prefix=find_prefix)
- )
-
-
-def attach_file(filename, sub_path):
- path = os.path.join(TEST_ROOT, sub_path, filename)
- world.browser.execute_script("$('input.file-input').css('display', 'block')")
- assert os.path.exists(path)
- world.browser.attach_file('file', os.path.abspath(path))
-
-
-def upload_file(filename, sub_path=''):
- # The file upload dialog is a faux modal, a div that takes over the display
- attach_file(filename, sub_path)
- modal_css = 'div.wrapper-modal-window-assetupload'
- button_css = u'{} .action-upload'.format(modal_css)
- world.css_click(button_css)
-
- # Clicking the Upload button triggers an AJAX POST.
- world.wait_for_ajax_complete()
-
- # The modal stays up with a "File uploaded succeeded" confirmation message, then goes away.
- # It should take under 2 seconds, so wait up to 10.
- # Note that is_css_not_present will return as soon as the element is gone.
- assert world.is_css_not_present(modal_css, wait_time=10)
-
-
-@step(u'"([^"]*)" logs in$')
-def other_user_login(step, name):
- step.given('I log out')
- world.visit('/')
-
- signin_css = 'a.action-signin'
- world.is_css_present(signin_css)
- world.css_click(signin_css)
-
- def fill_login_form():
- login_form = world.browser.find_by_css('form#login_form')
- login_form.find_by_name('email').fill(name + '@edx.org')
- login_form.find_by_name('password').fill("test")
- login_form.find_by_name('submit').click()
- world.retry_on_exception(fill_login_form)
- assert world.is_css_present('.new-course-button')
- world.scenario_dict['USER'] = get_user(name + '@edx.org')
-
-
-@step(u'the user "([^"]*)" exists( as a course (admin|staff member|is_staff))?$')
-def create_other_user(_step, name, has_extra_perms, role_name):
- email = name + '@edx.org'
- user = create_studio_user(uname=name, password="test", email=email)
- if has_extra_perms:
- if role_name == "is_staff":
- GlobalStaff().add_users(user)
- else:
- if role_name == "admin":
- # admins get staff privileges, as well
- roles = (CourseStaffRole, CourseInstructorRole)
- else:
- roles = (CourseStaffRole,)
- course_key = world.scenario_dict["COURSE"].id
- global_admin = AdminFactory()
- for role in roles:
- auth.add_users(global_admin, role(course_key), user)
-
-
-@step('I log out')
-def log_out(_step):
- world.visit('logout')
diff --git a/cms/djangoapps/contentstore/features/component.py b/cms/djangoapps/contentstore/features/component.py
deleted file mode 100644
index 0194f1ca33..0000000000
--- a/cms/djangoapps/contentstore/features/component.py
+++ /dev/null
@@ -1,181 +0,0 @@
-# pylint: disable=missing-docstring
-# pylint: disable=no-member
-# pylint: disable=redefined-outer-name
-
-# Lettuce formats proposed definitions for unimplemented steps with the
-# argument name "step" instead of "_step" and pylint does not like that.
-# pylint: disable=unused-argument
-
-from lettuce import step, world
-from openedx.core.lib.tests.tools import assert_equal, assert_in, assert_true # pylint: disable=no-name-in-module
-
-DISPLAY_NAME = "Display Name"
-
-
-@step(u'I add this type of single step component:$')
-def add_a_single_step_component(step):
- for step_hash in step.hashes:
- component = step_hash['Component']
- assert_in(component, ['Discussion', 'Video'])
-
- world.create_component_instance(
- step=step,
- category='{}'.format(component.lower()),
- )
-
-
-@step(u'I see this type of single step component:$')
-def see_a_single_step_component(step):
- for step_hash in step.hashes:
- component = step_hash['Component']
- assert_in(component, ['Discussion', 'Video'])
- component_css = '.xmodule_{}Module'.format(component)
- assert_true(world.is_css_present(component_css),
- u"{} couldn't be found".format(component))
-
-
-@step(u'I add this type of( Advanced)? (HTML|Problem) component:$')
-def add_a_multi_step_component(step, is_advanced, category):
- for step_hash in step.hashes:
- world.create_component_instance(
- step=step,
- category='{}'.format(category.lower()),
- component_type=step_hash['Component'],
- is_advanced=bool(is_advanced),
- )
-
-
-@step(u'I see (HTML|Problem) components in this order:')
-def see_a_multi_step_component(step, category):
-
- # Wait for all components to finish rendering
- if category == 'HTML':
- selector = 'li.studio-xblock-wrapper div.xblock-student_view'
- else:
- selector = 'li.studio-xblock-wrapper div.xblock-author_view'
- world.wait_for(lambda _: len(world.css_find(selector)) == len(step.hashes))
-
- for idx, step_hash in enumerate(step.hashes):
- if category == 'HTML':
- html_matcher = {
- 'Text': '\n \n',
- 'Announcement': '
Announcement Date
',
- 'Zooming Image Tool': 'Zooming Image Tool
',
- 'E-text Written in LaTeX': 'Example: E-text page
',
- 'Raw HTML': 'This template is similar to the Text template. The only difference is',
- }
- actual_html = world.css_html(selector, index=idx)
- assert_in(html_matcher[step_hash['Component']].strip(), actual_html.strip())
- else:
- actual_text = world.css_text(selector, index=idx)
- assert_in(step_hash['Component'], actual_text)
-
-
-@step(u'I see a "([^"]*)" Problem component$')
-def see_a_problem_component(step, category):
- component_css = '.xmodule_CapaModule'
- assert_true(world.is_css_present(component_css),
- 'No problem was added to the unit.')
-
- problem_css = '.studio-xblock-wrapper .xblock-student_view'
- # This view presents the given problem component in uppercase. Assert that the text matches
- # the component selected
- assert_true(world.css_contains_text(problem_css, category))
-
-
-@step(u'I add a "([^"]*)" "([^"]*)" component$')
-def add_component_category(step, component, category):
- assert category in ('single step', 'HTML', 'Problem', 'Advanced Problem')
- given_string = u'I add this type of {} component:'.format(category)
- step.given('{}\n{}\n{}'.format(given_string, '|Component|', '|{}|'.format(component)))
-
-
-@step(u'I delete all components$')
-def delete_all_components(step):
- count = len(world.css_find('.reorderable-container .studio-xblock-wrapper'))
- step.given('I delete "' + str(count) + '" component')
-
-
-@step(u'I delete "([^"]*)" component$')
-def delete_components(step, number):
- world.wait_for_xmodule()
- delete_btn_css = '.delete-button'
- prompt_css = '#prompt-warning'
- btn_css = u'{} .action-primary'.format(prompt_css)
- saving_mini_css = '#page-notification .wrapper-notification-mini'
- for _ in range(int(number)):
- world.css_click(delete_btn_css)
- assert_true(
- world.is_css_present('{}.is-shown'.format(prompt_css)),
- msg='Waiting for the confirmation prompt to be shown')
-
- # Pressing the button via css was not working reliably for the last component
- # when run in Chrome.
- if world.browser.driver_name == 'Chrome':
- world.browser.execute_script("$('{}').click()".format(btn_css))
- else:
- world.css_click(btn_css)
-
- # Wait for the saving notification to pop up then disappear
- if world.is_css_present('{}.is-shown'.format(saving_mini_css)):
- world.css_find('{}.is-hiding'.format(saving_mini_css))
-
-
-@step(u'I see no components')
-def see_no_components(steps):
- assert world.is_css_not_present('li.studio-xblock-wrapper')
-
-
-@step(u'I delete a component')
-def delete_one_component(step):
- world.css_click('.delete-button')
-
-
-@step(u'I edit and save a component')
-def edit_and_save_component(step):
- world.css_click('.edit-button')
- world.css_click('.save-button')
-
-
-@step(u'I duplicate the (first|second|third) component$')
-def duplicated_component(step, ordinal):
- ord_map = {
- "first": 0,
- "second": 1,
- "third": 2,
- }
- index = ord_map[ordinal]
- duplicate_btn_css = '.duplicate-button'
- world.css_click(duplicate_btn_css, int(index))
-
-
-@step(u'I see a Problem component with display name "([^"]*)" in position "([^"]*)"$')
-def see_component_in_position(step, display_name, index):
- component_css = '.xmodule_CapaModule'
-
- def find_problem(_driver):
- return world.css_text(component_css, int(index)).startswith(display_name)
-
- world.wait_for(find_problem, timeout_msg='Did not find the duplicated problem')
-
-
-@step(u'I see the display name is "([^"]*)"')
-def check_component_display_name(step, display_name):
- # The display name for the unit uses the same structure, must differentiate by level-element.
- label = world.css_html(".level-element>header>div>div>span.xblock-display-name")
- assert_equal(display_name, label)
-
-
-@step(u'I change the display name to "([^"]*)"')
-def change_display_name(step, display_name):
- world.edit_component_and_select_settings()
- index = world.get_setting_entry_index(DISPLAY_NAME)
- world.set_field_value(index, display_name)
- world.save_component()
-
-
-@step(u'I unset the display name')
-def unset_display_name(step):
- world.edit_component_and_select_settings()
- world.revert_setting_entry(DISPLAY_NAME)
- world.save_component()
diff --git a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py
deleted file mode 100644
index a066fb145f..0000000000
--- a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py
+++ /dev/null
@@ -1,270 +0,0 @@
-# disable missing docstring
-# pylint: disable=missing-docstring
-# pylint: disable=no-member
-
-from lettuce import world
-from selenium.webdriver.common.keys import Keys
-
-from common import type_in_codemirror
-from openedx.core.lib.tests.tools import assert_equal, assert_in # pylint: disable=no-name-in-module
-from terrain.steps import reload_the_page
-
-
-@world.absorb
-def create_component_instance(step, category, component_type=None, is_advanced=False, advanced_component=None):
- """
- Create a new component in a Unit.
-
- Parameters
- ----------
- category: component type (discussion, html, problem, video, advanced)
- component_type: for components with multiple templates, the link text in the menu
- is_advanced: for problems, is the desired component under the advanced menu?
- advanced_component: for advanced components, the related value of policy key 'advanced_modules'
- """
- assert_in(category, ['advanced', 'problem', 'html', 'video', 'discussion'])
-
- component_button_css = 'span.large-{}-icon'.format(category.lower())
- if category == 'problem':
- module_css = 'div.xmodule_CapaModule'
- elif category == 'advanced':
- module_css = 'div.xmodule_{}Module'.format(advanced_component.title())
- elif category == 'discussion':
- module_css = 'div.xblock-author_view-{}'.format(category.lower())
- else:
- module_css = 'div.xmodule_{}Module'.format(category.title())
-
- # Count how many of that module is on the page. Later we will
- # assert that one more was added.
- # We need to use world.browser.find_by_css instead of world.css_find
- # because it's ok if there are currently zero of them.
- module_count_before = len(world.browser.find_by_css(module_css))
-
- # Disable the jquery animation for the transition to the menus.
- world.disable_jquery_animations()
- world.css_click(component_button_css)
-
- if category in ('problem', 'html', 'advanced'):
- world.wait_for_invisible(component_button_css)
- click_component_from_menu(category, component_type, is_advanced)
-
- expected_count = module_count_before + 1
- world.wait_for(
- lambda _: len(world.css_find(module_css)) == expected_count,
- timeout=20
- )
-
-
-@world.absorb
-def click_new_component_button(step, component_button_css):
- step.given('I have clicked the new unit button')
-
- world.css_click(component_button_css)
-
-
-def _click_advanced():
- css = 'ul.problem-type-tabs a[href="#tab2"]'
- world.css_click(css)
-
- # Wait for the advanced tab items to be displayed
- tab2_css = 'div.ui-tabs-panel#tab2'
- world.wait_for_visible(tab2_css)
-
-
-def _find_matching_button(category, component_type):
- """
- Find the button with the specified text. There should be one and only one.
- """
-
- # The tab shows buttons for the given category
- buttons = world.css_find(u'div.new-component-{} button'.format(category))
-
- # Find the button whose text matches what you're looking for
- matched_buttons = [btn for btn in buttons if btn.text == component_type]
-
- # There should be one and only one
- assert_equal(len(matched_buttons), 1)
- return matched_buttons[0]
-
-
-def click_component_from_menu(category, component_type, is_advanced):
- """
- Creates a component for a category with more
- than one template, i.e. HTML and Problem.
- For some problem types, it is necessary to click to
- the Advanced tab.
- The component_type is the link text, e.g. "Blank Common Problem"
- """
- if is_advanced:
- # Sometimes this click does not work if you go too fast.
- world.retry_on_exception(
- _click_advanced,
- ignored_exceptions=AssertionError,
- )
-
- # Retry this in case the list is empty because you tried too fast.
- link = world.retry_on_exception(
- lambda: _find_matching_button(category, component_type),
- ignored_exceptions=AssertionError
- )
-
- # Wait for the link to be clickable. If you go too fast it is not.
- world.retry_on_exception(lambda: link.click())
-
-
-@world.absorb
-def edit_component_and_select_settings():
- world.edit_component()
- world.ensure_settings_visible()
-
-
-@world.absorb
-def ensure_settings_visible():
- # Select the 'settings' tab if there is one (it isn't displayed if it is the only option)
- settings_button = world.browser.find_by_css('.settings-button')
- if settings_button:
- world.css_click('.settings-button')
-
-
-@world.absorb
-def edit_component(index=0):
- # Verify that the "loading" indication has been hidden.
- world.wait_for_loading()
- # Verify that the "edit" button is present.
- world.wait_for(lambda _driver: world.css_visible('.edit-button'))
- world.css_click('.edit-button', index)
- world.wait_for_ajax_complete()
-
-
-@world.absorb
-def select_editor_tab(tab_name):
- editor_tabs = world.browser.find_by_css('.editor-tabs a')
- expected_tab_text = tab_name.strip().upper()
- matching_tabs = [tab for tab in editor_tabs if tab.text.upper() == expected_tab_text]
- assert len(matching_tabs) == 1
- tab = matching_tabs[0]
- tab.click()
- world.wait_for_ajax_complete()
-
-
-def enter_xml_in_advanced_problem(_step, text):
- """
- Edits an advanced problem (assumes only on page),
- types the provided XML, and saves the component.
- """
- world.edit_component()
- type_in_codemirror(0, text)
- world.save_component()
-
-
-@world.absorb
-def verify_setting_entry(setting, display_name, value, explicitly_set):
- """
- Verify the capa module fields are set as expected in the
- Advanced Settings editor.
-
- Parameters
- ----------
- setting: the WebDriverElement object found in the browser
- display_name: the string expected as the label
- html: the expected field value
- explicitly_set: True if the value is expected to have been explicitly set
- for the problem, rather than derived from the defaults. This is verified
- by the existence of a "Clear" button next to the field value.
- """
- label_element = setting.find_by_css('.setting-label')[0]
- assert_equal(display_name, label_element.html.strip())
- label_for = label_element['for']
-
- # Check if the web object is a list type
- # If so, we use a slightly different mechanism for determining its value
- if setting.has_class('metadata-list-enum') or setting.has_class('metadata-dict') or setting.has_class('metadata-video-translations'):
- list_value = ', '.join(ele.value for ele in setting.find_by_css('.list-settings-item'))
- assert_equal(value, list_value)
- elif setting.has_class('metadata-videolist-enum'):
- list_value = ', '.join(ele.find_by_css('input')[0].value for ele in setting.find_by_css('.videolist-settings-item'))
- assert_equal(value, list_value)
- else:
- assert_equal(value, setting.find_by_id(label_for).value)
-
- # VideoList doesn't have clear button
- if not setting.has_class('metadata-videolist-enum'):
- settingClearButton = setting.find_by_css('.setting-clear')[0]
- assert_equal(explicitly_set, settingClearButton.has_class('active'))
- assert_equal(not explicitly_set, settingClearButton.has_class('inactive'))
-
-
-@world.absorb
-def verify_all_setting_entries(expected_entries):
- settings = world.browser.find_by_css('.wrapper-comp-setting')
- assert_equal(len(expected_entries), len(settings))
- for (counter, setting) in enumerate(settings):
- world.verify_setting_entry(
- setting, expected_entries[counter][0],
- expected_entries[counter][1], expected_entries[counter][2]
- )
-
-
-@world.absorb
-def save_component():
- world.css_click("a.action-save,a.save-button")
- world.wait_for_ajax_complete()
-
-
-@world.absorb
-def save_component_and_reopen(step):
- save_component()
- # We have a known issue that modifications are still shown within the edit window after cancel (though)
- # they are not persisted. Refresh the browser to make sure the changes WERE persisted after Save.
- reload_the_page(step)
- edit_component_and_select_settings()
-
-
-@world.absorb
-def cancel_component(step):
- world.css_click("a.action-cancel")
- # We have a known issue that modifications are still shown within the edit window after cancel (though)
- # they are not persisted. Refresh the browser to make sure the changes were not persisted.
- reload_the_page(step)
-
-
-@world.absorb
-def revert_setting_entry(label):
- get_setting_entry(label).find_by_css('.setting-clear')[0].click()
-
-
-@world.absorb
-def get_setting_entry(label):
- def get_setting():
- settings = world.css_find('.wrapper-comp-setting')
- for setting in settings:
- if setting.find_by_css('.setting-label')[0].value == label:
- return setting
- return None
- return world.retry_on_exception(get_setting)
-
-
-@world.absorb
-def get_setting_entry_index(label):
- def get_index():
- settings = world.css_find('.wrapper-comp-setting')
- for index, setting in enumerate(settings):
- if setting.find_by_css('.setting-label')[0].value == label:
- return index
- return None
- return world.retry_on_exception(get_index)
-
-
-@world.absorb
-def set_field_value(index, value):
- """
- Set the field to the specified value.
-
- Note: we cannot use css_fill here because the value is not set
- until after you move away from that field.
- Instead we will find the element, set its value, then hit the Tab key
- to get to the next field.
- """
- elem = world.css_find('div.wrapper-comp-setting input')[index]
- elem.value = value
- elem.type(Keys.TAB)
diff --git a/cms/djangoapps/contentstore/features/course-settings.py b/cms/djangoapps/contentstore/features/course-settings.py
deleted file mode 100644
index 54fb036ef6..0000000000
--- a/cms/djangoapps/contentstore/features/course-settings.py
+++ /dev/null
@@ -1,170 +0,0 @@
-# pylint: disable=missing-docstring
-# pylint: disable=no-member
-# pylint: disable=redefined-outer-name
-
-from django.conf import settings
-from lettuce import step, world
-from selenium.webdriver.common.keys import Keys
-
-from cms.djangoapps.contentstore.features.common import type_in_codemirror
-
-TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
-
-COURSE_START_DATE_CSS = "#course-start-date"
-COURSE_END_DATE_CSS = "#course-end-date"
-ENROLLMENT_START_DATE_CSS = "#course-enrollment-start-date"
-ENROLLMENT_END_DATE_CSS = "#course-enrollment-end-date"
-
-COURSE_START_TIME_CSS = "#course-start-time"
-COURSE_END_TIME_CSS = "#course-end-time"
-ENROLLMENT_START_TIME_CSS = "#course-enrollment-start-time"
-ENROLLMENT_END_TIME_CSS = "#course-enrollment-end-time"
-
-DUMMY_TIME = "15:30"
-DEFAULT_TIME = "00:00"
-
-
-############### ACTIONS ####################
-@step('I select Schedule and Details$')
-def test_i_select_schedule_and_details(_step):
- world.click_course_settings()
- link_css = 'li.nav-course-settings-schedule a'
- world.css_click(link_css)
- world.wait_for_requirejs(
- ["jquery", "js/models/course",
- "js/models/settings/course_details", "js/views/settings/main"])
-
-
-@step('I have set course dates$')
-def test_i_have_set_course_dates(step):
- step.given('I have opened a new course in Studio')
- step.given('I select Schedule and Details')
- step.given('And I set course dates')
-
-
-@step('And I set course dates$')
-def test_and_i_set_course_dates(_step):
- set_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
- set_date_or_time(COURSE_END_DATE_CSS, '12/26/2013')
- set_date_or_time(ENROLLMENT_START_DATE_CSS, '12/1/2013')
- set_date_or_time(ENROLLMENT_END_DATE_CSS, '12/10/2013')
-
- set_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
- set_date_or_time(ENROLLMENT_END_TIME_CSS, DUMMY_TIME)
-
-
-@step('And I clear all the dates except start$')
-def test_and_i_clear_all_the_dates_except_start(_step):
- set_date_or_time(COURSE_END_DATE_CSS, '')
- set_date_or_time(ENROLLMENT_START_DATE_CSS, '')
- set_date_or_time(ENROLLMENT_END_DATE_CSS, '')
-
-
-@step('Then I see cleared dates$')
-def test_then_i_see_cleared_dates(_step):
- verify_date_or_time(COURSE_END_DATE_CSS, '')
- verify_date_or_time(ENROLLMENT_START_DATE_CSS, '')
- verify_date_or_time(ENROLLMENT_END_DATE_CSS, '')
-
- verify_date_or_time(COURSE_END_TIME_CSS, '')
- verify_date_or_time(ENROLLMENT_START_TIME_CSS, '')
- verify_date_or_time(ENROLLMENT_END_TIME_CSS, '')
-
- # Verify course start date (required) and time still there
- verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
- verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
-
-
-@step('I clear the course start date$')
-def test_i_clear_the_course_start_date(_step):
- set_date_or_time(COURSE_START_DATE_CSS, '')
-
-
-@step('I receive a warning about course start date$')
-def test_i_receive_a_warning_about_course_start_date(_step):
- assert world.css_has_text('.message-error', 'The course must have an assigned start date.')
- assert 'error' in world.css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class') # pylint: disable=protected-access
- assert 'error' in world.css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class') # pylint: disable=protected-access
-
-
-@step('the previously set start date is shown$')
-def test_the_previously_set_start_date_is_shown(_step):
- verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
- verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
-
-
-@step('Given I have tried to clear the course start$')
-def test_i_have_tried_to_clear_the_course_start(step):
- step.given("I have set course dates")
- step.given("I clear the course start date")
- step.given("I receive a warning about course start date")
-
-
-@step('I have entered a new course start date$')
-def test_i_have_entered_a_new_course_start_date(_step):
- set_date_or_time(COURSE_START_DATE_CSS, '12/22/2013')
-
-
-@step('The warning about course start date goes away$')
-def test_the_warning_about_course_start_date_goes_away(_step):
- assert world.is_css_not_present('.message-error')
- assert 'error' not in world.css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class') # pylint: disable=protected-access
- assert 'error' not in world.css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class') # pylint: disable=protected-access
-
-
-@step('my new course start date is shown$')
-def new_course_start_date_is_shown(_step):
- verify_date_or_time(COURSE_START_DATE_CSS, '12/22/2013')
- # Time should have stayed from before attempt to clear date.
- verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
-
-
-@step('I change fields$')
-def test_i_change_fields(_step):
- set_date_or_time(COURSE_START_DATE_CSS, '7/7/7777')
- set_date_or_time(COURSE_END_DATE_CSS, '7/7/7777')
- set_date_or_time(ENROLLMENT_START_DATE_CSS, '7/7/7777')
- set_date_or_time(ENROLLMENT_END_DATE_CSS, '7/7/7777')
-
-
-@step('I change the course overview')
-def test_change_course_overview(_step):
- type_in_codemirror(0, "
Overview
")
-
-
-############### HELPER METHODS ####################
-def set_date_or_time(css, date_or_time):
- """
- Sets date or time field.
- """
- world.css_fill(css, date_or_time)
- e = world.css_find(css).first
- # hit Enter to apply the changes
- e._element.send_keys(Keys.ENTER) # pylint: disable=protected-access
-
-
-def verify_date_or_time(css, date_or_time):
- """
- Verifies date or time field.
- """
- # We need to wait for JavaScript to fill in the field, so we use
- # css_has_value(), which first checks that the field is not blank
- assert world.css_has_value(css, date_or_time)
-
-
-@step('I do not see the changes')
-@step('I see the set dates')
-def i_see_the_set_dates(_step):
- """
- Ensure that each field has the value set in `test_and_i_set_course_dates`.
- """
- verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
- verify_date_or_time(COURSE_END_DATE_CSS, '12/26/2013')
- verify_date_or_time(ENROLLMENT_START_DATE_CSS, '12/01/2013')
- verify_date_or_time(ENROLLMENT_END_DATE_CSS, '12/10/2013')
-
- verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
- # Unset times get set to 12 AM once the corresponding date has been set.
- verify_date_or_time(COURSE_END_TIME_CSS, DEFAULT_TIME)
- verify_date_or_time(ENROLLMENT_START_TIME_CSS, DEFAULT_TIME)
- verify_date_or_time(ENROLLMENT_END_TIME_CSS, DUMMY_TIME)
diff --git a/cms/djangoapps/contentstore/features/course_import.py b/cms/djangoapps/contentstore/features/course_import.py
deleted file mode 100644
index e75db207bc..0000000000
--- a/cms/djangoapps/contentstore/features/course_import.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# pylint: disable=missing-docstring
-# pylint: disable=redefined-outer-name
-# pylint: disable=unused-argument
-
-import os
-
-from django.conf import settings
-from lettuce import step, world
-
-
-def import_file(filename):
- world.browser.execute_script("$('input.file-input').css('display', 'block')")
- path = os.path.join(settings.COMMON_TEST_DATA_ROOT, "imports", filename)
- world.browser.attach_file('course-data', os.path.abspath(path))
- world.css_click('input.submit-button')
- # Go to course outline
- world.click_course_content()
- outline_css = 'li.nav-course-courseware-outline a'
- world.css_click(outline_css)
-
-
-@step('I go to the import page$')
-def go_to_import(step):
- menu_css = 'li.nav-course-tools'
- import_css = 'li.nav-course-tools-import a'
- world.css_click(menu_css)
- world.css_click(import_css)
diff --git a/cms/djangoapps/contentstore/features/html-editor.feature b/cms/djangoapps/contentstore/features/html-editor.feature
deleted file mode 100644
index 7b71471679..0000000000
--- a/cms/djangoapps/contentstore/features/html-editor.feature
+++ /dev/null
@@ -1,128 +0,0 @@
-@shard_2
-Feature: CMS.HTML Editor
- As a course author, I want to be able to create HTML blocks.
-
- Scenario: User can view metadata
- Given I have created a Blank HTML Page
- And I edit and select Settings
- Then I see the HTML component settings
-
- # Safari doesn't save the name properly
- @skip_safari
- Scenario: User can modify display name
- Given I have created a Blank HTML Page
- And I edit and select Settings
- Then I can modify the display name
- And my display name change is persisted on save
-
- Scenario: TinyMCE link plugin sets urls correctly
- Given I have created a Blank HTML Page
- When I edit the page
- And I add a link with static link "/static/image.jpg" via the Link Plugin Icon
- Then the href link is rewritten to the asset link "image.jpg"
- And the link is shown as "/static/image.jpg" in the Link Plugin
-
- Scenario: TinyMCE and CodeMirror preserve style tags
- Given I have created a Blank HTML Page
- When I edit the page
- And type "pages
" in the code editor and press OK
- And I save the page
- Then the page text contains:
- """
- pages
-
- """
-
- Scenario: TinyMCE and CodeMirror preserve span tags
- Given I have created a Blank HTML Page
- When I edit the page
- And type "Test" in the code editor and press OK
- And I save the page
- Then the page text contains:
- """
- Test
- """
-
- Scenario: TinyMCE and CodeMirror preserve math tags
- Given I have created a Blank HTML Page
- When I edit the page
- And type "" in the code editor and press OK
- And I save the page
- Then the page text contains:
- """
-
- """
-
- Scenario: TinyMCE toolbar buttons are as expected
- Given I have created a Blank HTML Page
- When I edit the page
- Then the expected toolbar buttons are displayed
-
- Scenario: Static links are converted when switching between code editor and WYSIWYG views
- Given I have created a Blank HTML Page
- When I edit the page
- And type "
" in the code editor and press OK
- Then the src link is rewritten to the asset link "image.jpg"
- And the code editor displays "
"
-
- Scenario: Code format toolbar button wraps text with code tags
- Given I have created a Blank HTML Page
- When I edit the page
- And I set the text to "display as code" and I select the text
- And I select the code toolbar button
- And I save the page
- Then the page text contains:
- """
- display as code
- """
-
- Scenario: Raw HTML component does not change text
- Given I have created a raw HTML component
- When I edit the page
- And type "zzzz" into the Raw Editor
- And I save the page
- Then the page text contains:
- """
- - zzzz
- """
- And I edit the page
- Then the Raw Editor contains exactly:
- """
- - zzzz
- """
-
- Scenario: Font selection dropdown contains Default font and tinyMCE builtin fonts
- Given I have created a Blank HTML Page
- When I edit the page
- And I click font selection dropdown
- Then I should see a list of available fonts
- And "Default" option sets the expected font family
- And all standard tinyMCE fonts should be available
-
-# Skipping in master due to brittleness JZ 05/22/2014
-# Scenario: Can switch from Visual Editor to Raw
-# Given I have created a Blank HTML Page
-# When I edit the component and select the Raw Editor
-# And I save the page
-# When I edit the page
-# And type "fancy html" into the Raw Editor
-# And I save the page
-# Then the page text contains:
-# """
-# fancy html
-# """
-
-# Skipping in master due to brittleness JZ 05/22/2014
-# Scenario: Can switch from Raw Editor to Visual
-# Given I have created a raw HTML component
-# And I edit the component and select the Visual Editor
-# And I save the page
-# When I edit the page
-# And type "less fancy html" in the code editor and press OK
-# And I save the page
-# Then the page text contains:
-# """
-# less fancy html
-# """
diff --git a/cms/djangoapps/contentstore/features/html-editor.py b/cms/djangoapps/contentstore/features/html-editor.py
deleted file mode 100644
index 490e7a0efb..0000000000
--- a/cms/djangoapps/contentstore/features/html-editor.py
+++ /dev/null
@@ -1,306 +0,0 @@
-# disable missing docstring
-# pylint: disable=missing-docstring
-# pylint: disable=no-member
-
-from collections import OrderedDict
-
-from lettuce import step, world
-
-from common import get_codemirror_value, type_in_codemirror
-from openedx.core.lib.tests.tools import assert_equal, assert_in # pylint: disable=no-name-in-module
-
-CODEMIRROR_SELECTOR_PREFIX = "$('iframe').contents().find"
-
-
-@step('I have created a Blank HTML Page$')
-def i_created_blank_html_page(step):
- step.given('I am in Studio editing a new unit')
- world.create_component_instance(
- step=step,
- category='html',
- component_type='Text'
- )
-
-
-@step('I have created a raw HTML component')
-def i_created_raw_html(step):
- step.given('I am in Studio editing a new unit')
- world.create_component_instance(
- step=step,
- category='html',
- component_type='Raw HTML'
- )
-
-
-@step('I see the HTML component settings$')
-def i_see_only_the_html_display_name(_step):
- world.verify_all_setting_entries(
- [
- ['Display Name', "Text", False],
- ['Editor', "Visual", False]
- ]
- )
-
-
-@step('I have created an E-text Written in LaTeX$')
-def i_created_etext_in_latex(step):
- step.given('I am in Studio editing a new unit')
- step.given('I have enabled latex compiler')
- world.create_component_instance(
- step=step,
- category='html',
- component_type='E-text Written in LaTeX'
- )
-
-
-@step('I edit the page$')
-def i_click_on_edit_icon(_step):
- world.edit_component()
-
-
-@step('I add a link with static link "(.*)" via the Link Plugin Icon$')
-def i_click_on_link_plugin_icon(_step, path):
- def fill_in_link_fields():
- world.css_fill('.mce-textbox', path, 0)
- world.css_fill('.mce-textbox', 'picture', 1)
-
- use_plugin('.mce-i-link', fill_in_link_fields)
-
-
-@step('the link is shown as "(.*)" in the Link Plugin$')
-def check_link_in_link_plugin(_step, path):
- # Ensure caret position is within the link just created.
- script = """
- var editor = tinyMCE.activeEditor;
- editor.selection.select(editor.dom.select('a')[0]);"""
- world.browser.driver.execute_script(script)
- world.wait_for_ajax_complete()
-
- use_plugin(
- '.mce-i-link',
- lambda: assert_equal(path, world.css_find('.mce-textbox')[0].value)
- )
-
-
-@step('type "(.*)" in the code editor and press OK$')
-def type_in_codemirror_plugin(_step, text):
- # Verify that raw code editor is not visible.
- assert world.css_has_class('.CodeMirror', 'is-inactive')
- # Verify that TinyMCE editor is present
- assert world.is_css_present('.tiny-mce')
- use_code_editor(
- lambda: type_in_codemirror(0, text, CODEMIRROR_SELECTOR_PREFIX)
- )
-
-
-@step('and the code editor displays "(.*)"$')
-def verify_code_editor_text(_step, text):
- use_code_editor(
- lambda: assert_equal(text, get_codemirror_value(0, CODEMIRROR_SELECTOR_PREFIX))
- )
-
-
-@step('I save the page$')
-def i_click_on_save(_step):
- world.save_component()
-
-
-@step('the page text contains:')
-def check_page_text(step):
- assert_in(step.multiline, world.css_find('.xmodule_HtmlModule').html)
-
-
-@step('the Raw Editor contains exactly:')
-def check_raw_editor_text(step):
- assert_equal(step.multiline, get_codemirror_value(0))
-
-
-@step('the src link is rewritten to the asset link "(.*)"$')
-def image_static_link_is_rewritten(_step, path):
- # Find the TinyMCE iframe within the main window
- with world.browser.get_iframe('mce_0_ifr') as tinymce:
- image = tinymce.find_by_tag('img').first
- assert_in(unicode(world.scenario_dict['COURSE'].id.make_asset_key('asset', path)), image['src'])
-
-
-@step('the href link is rewritten to the asset link "(.*)"$')
-def link_static_link_is_rewritten(_step, path):
- # Find the TinyMCE iframe within the main window
- with world.browser.get_iframe('mce_0_ifr') as tinymce:
- link = tinymce.find_by_tag('a').first
- assert_in(unicode(world.scenario_dict['COURSE'].id.make_asset_key('asset', path)), link['href'])
-
-
-@step('the expected toolbar buttons are displayed$')
-def check_toolbar_buttons(_step):
- dropdowns = world.css_find('.mce-listbox')
- assert_equal(2, len(dropdowns))
-
- # Format dropdown
- assert_equal('Paragraph', dropdowns[0].text)
- # Font dropdown
- assert_equal('Font Family', dropdowns[1].text)
-
- buttons = world.css_find('.mce-ico')
-
- # Note that the code editor icon is not present because we are now showing text instead of an icon.
- # However, other test points user the code editor, so we have already verified its presence.
- expected_buttons = [
- 'bold',
- 'italic',
- 'underline',
- 'forecolor',
- # This is our custom "code style" button, which uses an image instead of a class.
- 'none',
- 'alignleft',
- 'aligncenter',
- 'alignright',
- 'alignjustify',
- 'bullist',
- 'numlist',
- 'outdent',
- 'indent',
- 'blockquote',
- 'link',
- 'unlink',
- 'image'
- ]
-
- assert_equal(len(expected_buttons), len(buttons))
-
- for index, button in enumerate(expected_buttons):
- class_names = buttons[index]._element.get_attribute('class') # pylint: disable=protected-access
- assert_equal("mce-ico mce-i-" + button, class_names)
-
-
-@step('I set the text to "(.*)" and I select the text$')
-def set_text_and_select(_step, text):
- script = """
- var editor = tinyMCE.activeEditor;
- editor.setContent(arguments[0]);
- editor.selection.select(editor.dom.select('p')[0]);"""
- world.browser.driver.execute_script(script, str(text))
- world.wait_for_ajax_complete()
-
-
-@step('I select the code toolbar button$')
-def select_code_button(_step):
- # This is our custom "code style" button. It uses an image instead of a class.
- world.css_click(".mce-i-none")
-
-
-@step('type "(.*)" into the Raw Editor$')
-def type_in_raw_editor(_step, text):
- # Verify that CodeMirror editor is not hidden
- assert not world.css_has_class('.CodeMirror', 'is-inactive')
- # Verify that TinyMCE Editor is not present
- assert world.is_css_not_present('.tiny-mce')
- type_in_codemirror(0, text)
-
-
-@step('I edit the component and select the (Raw|Visual) Editor$')
-def select_editor(_step, editor):
- world.edit_component_and_select_settings()
- world.browser.select('Editor', editor)
-
-
-@step('I click font selection dropdown')
-def click_font_dropdown(_step):
- dropdowns = [drop for drop in world.css_find('.mce-listbox') if drop.text == 'Font Family']
- assert_equal(len(dropdowns), 1)
- dropdowns[0].click()
-
-
-@step('I should see a list of available fonts')
-def font_selector_dropdown_is_shown(_step):
- font_panel = get_fonts_list_panel(world)
- expected_fonts = list(CUSTOM_FONTS.keys()) + list(TINYMCE_FONTS.keys())
- actual_fonts = [font.strip() for font in font_panel.text.split('\n')]
- assert_equal(actual_fonts, expected_fonts)
-
-
-@step('"Default" option sets the expected font family')
-def default_options_sets_expected_font_family(step): # pylint: disable=unused-argument, redefined-outer-name
- fonts = get_available_fonts(get_fonts_list_panel(world))
- fonts_found = fonts.get("Default", None)
- expected_font_family = CUSTOM_FONTS.get('Default')
- for expected_font in expected_font_family:
- assert_in(expected_font, fonts_found)
-
-
-@step('all standard tinyMCE fonts should be available')
-def check_standard_tinyMCE_fonts(_step):
- fonts = get_available_fonts(get_fonts_list_panel(world))
- for label, expected_fonts in TINYMCE_FONTS.items():
- for expected_font in expected_fonts:
- assert_in(expected_font, fonts.get(label, None))
-
-TINYMCE_FONTS = OrderedDict([
- ("Andale Mono", ['andale mono', 'times']),
- ("Arial", ['arial', 'helvetica', 'sans-serif']),
- ("Arial Black", ['arial black', 'avant garde']),
- ("Book Antiqua", ['book antiqua', 'palatino']),
- ("Comic Sans MS", ['comic sans ms', 'sans-serif']),
- ("Courier New", ['courier new', 'courier']),
- ("Georgia", ['georgia', 'palatino']),
- ("Helvetica", ['helvetica']),
- ("Impact", ['impact', 'chicago']),
- ("Symbol", ['symbol']),
- ("Tahoma", ['tahoma', 'arial', 'helvetica', 'sans-serif']),
- ("Terminal", ['terminal', 'monaco']),
- ("Times New Roman", ['times new roman', 'times']),
- ("Trebuchet MS", ['trebuchet ms', 'geneva']),
- ("Verdana", ['verdana', 'geneva']),
- # tinyMCE does not set font-family on dropdown span for these two fonts
- ("Webdings", [""]), # webdings
- ("Wingdings", [""]), # wingdings, 'zapf dingbats'
-])
-
-CUSTOM_FONTS = OrderedDict([
- ('Default', ['Open Sans', 'Verdana', 'Arial', 'Helvetica', 'sans-serif']),
-])
-
-
-def use_plugin(button_class, action):
- # Click on plugin button
- world.css_click(button_class)
- perform_action_in_plugin(action)
-
-
-def use_code_editor(action):
- # Click on plugin button
- buttons = world.css_find('div.mce-widget>button')
-
- code_editor = [button for button in buttons if button.text == 'HTML']
- assert_equal(1, len(code_editor))
- code_editor[0].click()
-
- perform_action_in_plugin(action)
-
-
-def perform_action_in_plugin(action):
- # Wait for the plugin window to open.
- world.wait_for_visible('.mce-window')
-
- # Trigger the action
- action()
-
- # Click OK
- world.css_click('.mce-primary')
-
-
-def get_fonts_list_panel(world):
- menus = world.css_find('.mce-menu')
- return menus[0]
-
-
-def get_available_fonts(font_panel):
- font_spans = font_panel.find_by_css('.mce-text')
- return {font_span.text: get_font_family(font_span) for font_span in font_spans}
-
-
-def get_font_family(font_span):
- # get_attribute('style').replace('font-family: ', '').replace(';', '') is equivalent to
- # value_of_css_property('font-family'). However, for reason unknown value_of_css_property fails tests in CI
- # while works as expected in local development environment
- return font_span._element.get_attribute('style').replace('font-family: ', '').replace(';', '') # pylint: disable=protected-access
diff --git a/cms/djangoapps/contentstore/features/problem-editor.feature b/cms/djangoapps/contentstore/features/problem-editor.feature
deleted file mode 100644
index a239d10901..0000000000
--- a/cms/djangoapps/contentstore/features/problem-editor.feature
+++ /dev/null
@@ -1,66 +0,0 @@
-@shard_1
-Feature: CMS.Problem Editor
- As a course author, I want to be able to create problems and edit their settings.
-
- Scenario: User can revert display name to unset
- Given I have created a Blank Common Problem
- When I edit and select Settings
- Then I can revert the display name to unset
- And my display name is unset on save
-
- Scenario: User can specify html in display name and it will be escaped
- Given I have created a Blank Common Problem
- When I edit and select Settings
- Then I can specify html in the display name and save
- And the problem display name is ""
-
- # IE will not click the revert button properly
- @skip_internetexplorer
- Scenario: User can select values in a Select
- Given I have created a Blank Common Problem
- When I edit and select Settings
- Then I can select Per Student for Randomization
- And my change to randomization is persisted
- And I can revert to the default value for randomization
-
- # Safari will input it as 35.
- @skip_safari
- Scenario: User can modify float input values
- Given I have created a Blank Common Problem
- When I edit and select Settings
- Then I can set the weight to "3.5"
- And my change to weight is persisted
- And I can revert to the default value of unset for weight
-
- Scenario: User cannot type letters in float number field
- Given I have created a Blank Common Problem
- When I edit and select Settings
- Then if I set the weight to "abc", it remains unset
-
- # Safari will input it as 234.
- @skip_safari
- Scenario: User cannot type decimal values integer number field
- Given I have created a Blank Common Problem
- When I edit and select Settings
- Then if I set the max attempts to "2.34", it will persist as a valid integer
-
- # Safari will input it incorrectly
- @skip_safari
- Scenario: User cannot type out of range values in an integer number field
- Given I have created a Blank Common Problem
- When I edit and select Settings
- Then if I set the max attempts to "-3", it will persist as a valid integer
-
- # Safari will input it as 35.
- @skip_safari
- Scenario: Settings changes are not saved on Cancel
- Given I have created a Blank Common Problem
- When I edit and select Settings
- Then I can set the weight to "3.5"
- And I can modify the display name
- Then If I press Cancel my changes are not persisted
-
- Scenario: Cheat sheet visible on toggle
- Given I have created a Blank Common Problem
- And I can edit the problem
- Then I can see cheatsheet
diff --git a/cms/djangoapps/contentstore/features/problem-editor.py b/cms/djangoapps/contentstore/features/problem-editor.py
deleted file mode 100644
index 72b46a733b..0000000000
--- a/cms/djangoapps/contentstore/features/problem-editor.py
+++ /dev/null
@@ -1,391 +0,0 @@
-# disable missing docstring
-# pylint: disable=missing-docstring
-# pylint: disable=no-member
-
-import json
-
-from lettuce import step, world
-from openedx.core.lib.tests.tools import assert_equal, assert_true # pylint: disable=no-name-in-module
-
-from cms.djangoapps.contentstore.features.advanced_settings import ADVANCED_MODULES_KEY, change_value
-from cms.djangoapps.contentstore.features.common import open_new_course, type_in_codemirror
-from cms.djangoapps.contentstore.features.course_import import import_file
-
-DISPLAY_NAME = "Display Name"
-MAXIMUM_ATTEMPTS = "Maximum Attempts"
-PROBLEM_WEIGHT = "Problem Weight"
-RANDOMIZATION = 'Randomization'
-SHOW_ANSWER = "Show Answer"
-SHOW_ANSWER_AFTER_SOME_NUMBER_OF_ATTEMPTS = 'Show Answer: Number of Attempts'
-SHOW_RESET_BUTTON = "Show Reset Button"
-TIMER_BETWEEN_ATTEMPTS = "Timer Between Attempts"
-MATLAB_API_KEY = "Matlab API key"
-
-
-@step('I have created a Blank Common Problem$')
-def i_created_blank_common_problem(step):
- step.given('I am in Studio editing a new unit')
- step.given("I have created another Blank Common Problem")
-
-
-@step('I have created a unit with advanced module "(.*)"$')
-def i_created_unit_with_advanced_module(step, advanced_module):
- step.given('I am in Studio editing a new unit')
-
- url = world.browser.url
- step.given("I select the Advanced Settings")
- change_value(step, ADVANCED_MODULES_KEY, '["{}"]'.format(advanced_module))
- world.visit(url)
- world.wait_for_xmodule()
-
-
-@step('I have created an advanced component "(.*)" of type "(.*)"')
-def i_create_new_advanced_component(step, component_type, advanced_component):
- world.create_component_instance(
- step=step,
- category='advanced',
- component_type=component_type,
- advanced_component=advanced_component
- )
-
-
-@step('I have created another Blank Common Problem$')
-def i_create_new_common_problem(step):
- world.create_component_instance(
- step=step,
- category='problem',
- component_type='Blank Common Problem'
- )
-
-
-@step('when I mouseover on "(.*)"')
-def i_mouseover_on_html_component(_step, element_class):
- action_css = '.{}'.format(element_class)
- world.trigger_event(action_css, event='mouseover')
-
-
-@step(u'I can see Reply to Annotation link$')
-def i_see_reply_to_annotation_link(_step):
- css_selector = 'a.annotatable-reply'
- world.wait_for_visible(css_selector)
-
-
-@step(u'I see that page has scrolled "(.*)" when I click on "(.*)" link$')
-def i_see_annotation_problem_page_scrolls(_step, scroll_direction, link_css):
- scroll_js = "$(window).scrollTop();"
- scroll_height_before = world.browser.evaluate_script(scroll_js)
- world.css_click("a.{}".format(link_css))
- scroll_height_after = world.browser.evaluate_script(scroll_js)
- if scroll_direction == "up":
- assert scroll_height_after < scroll_height_before
- elif scroll_direction == "down":
- assert scroll_height_after > scroll_height_before
-
-
-@step('I have created an advanced problem of type "(.*)"$')
-def i_create_new_advanced_problem(step, component_type):
- world.create_component_instance(
- step=step,
- category='problem',
- component_type=component_type,
- is_advanced=True
- )
-
-
-@step('I edit and select Settings$')
-def i_edit_and_select_settings(_step):
- world.edit_component_and_select_settings()
-
-
-@step('I see the advanced settings and their expected values$')
-def i_see_advanced_settings_with_values(_step):
- world.verify_all_setting_entries(
- [
- [DISPLAY_NAME, "Blank Common Problem", True],
- [MATLAB_API_KEY, "", False],
- [MAXIMUM_ATTEMPTS, "", False],
- [PROBLEM_WEIGHT, "", False],
- [RANDOMIZATION, "Never", False],
- [SHOW_ANSWER, "Finished", False],
- [SHOW_ANSWER_AFTER_SOME_NUMBER_OF_ATTEMPTS, '0', False],
- [SHOW_RESET_BUTTON, "False", False],
- [TIMER_BETWEEN_ATTEMPTS, "0", False],
- ])
-
-
-@step('I can modify the display name')
-def i_can_modify_the_display_name(_step):
- # Verifying that the display name can be a string containing a floating point value
- # (to confirm that we don't throw an error because it is of the wrong type).
- index = world.get_setting_entry_index(DISPLAY_NAME)
- world.set_field_value(index, '3.4')
- verify_modified_display_name()
-
-
-@step('my display name change is persisted on save')
-def my_display_name_change_is_persisted_on_save(step):
- world.save_component_and_reopen(step)
- verify_modified_display_name()
-
-
-@step('the problem display name is "(.*)"$')
-def verify_problem_display_name(_step, name):
- """
- name is uppercased because the heading styles are uppercase in css
- """
- assert_equal(name, world.browser.find_by_css('.problem-header').text)
-
-
-@step('I can specify special characters in the display name')
-def i_can_modify_the_display_name_with_special_chars(_step):
- index = world.get_setting_entry_index(DISPLAY_NAME)
- world.set_field_value(index, "updated ' \" &")
- verify_modified_display_name_with_special_chars()
-
-
-@step('I can specify html in the display name and save')
-def i_can_modify_the_display_name_with_html(_step):
- """
- If alert appear on save then UnexpectedAlertPresentException
- will occur and test will fail.
- """
- index = world.get_setting_entry_index(DISPLAY_NAME)
- world.set_field_value(index, "")
- verify_modified_display_name_with_html()
- world.save_component()
-
-
-@step('my special characters and persisted on save')
-def special_chars_persisted_on_save(step):
- world.save_component_and_reopen(step)
- verify_modified_display_name_with_special_chars()
-
-
-@step('I can revert the display name to unset')
-def can_revert_display_name_to_unset(_step):
- world.revert_setting_entry(DISPLAY_NAME)
- verify_unset_display_name()
-
-
-@step('my display name is unset on save')
-def my_display_name_is_persisted_on_save(step):
- world.save_component_and_reopen(step)
- verify_unset_display_name()
-
-
-@step('I can select Per Student for Randomization')
-def i_can_select_per_student_for_randomization(_step):
- world.browser.select(RANDOMIZATION, "Per Student")
- verify_modified_randomization()
-
-
-@step('my change to randomization is persisted')
-def my_change_to_randomization_is_persisted(step):
- world.save_component_and_reopen(step)
- verify_modified_randomization()
-
-
-@step('I can revert to the default value for randomization')
-def i_can_revert_to_default_for_randomization(step):
- world.revert_setting_entry(RANDOMIZATION)
- world.save_component_and_reopen(step)
- world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Never", False)
-
-
-@step('I can set the weight to "(.*)"?')
-def i_can_set_weight(_step, weight):
- set_weight(weight)
- verify_modified_weight()
-
-
-@step('my change to weight is persisted')
-def my_change_to_weight_is_persisted(step):
- world.save_component_and_reopen(step)
- verify_modified_weight()
-
-
-@step('I can revert to the default value of unset for weight')
-def i_can_revert_to_default_for_unset_weight(step):
- world.revert_setting_entry(PROBLEM_WEIGHT)
- world.save_component_and_reopen(step)
- world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", False)
-
-
-@step('if I set the weight to "(.*)", it remains unset')
-def set_the_weight_to_abc(step, bad_weight):
- set_weight(bad_weight)
- # We show the clear button immediately on type, hence the "True" here.
- world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", True)
- world.save_component_and_reopen(step)
- # But no change was actually ever sent to the model, so on reopen, explicitly_set is False
- world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", False)
-
-
-@step('if I set the max attempts to "(.*)", it will persist as a valid integer$')
-def set_the_max_attempts(step, max_attempts_set):
- # on firefox with selenium, the behavior is different.
- # eg 2.34 displays as 2.34 and is persisted as 2
- index = world.get_setting_entry_index(MAXIMUM_ATTEMPTS)
- world.set_field_value(index, max_attempts_set)
- world.save_component_and_reopen(step)
- value = world.css_value('input.setting-input', index=index)
- assert value != "", "max attempts is blank"
- assert int(value) >= 0
-
-
-@step('Edit High Level Source is not visible')
-def edit_high_level_source_not_visible(step):
- verify_high_level_source_links(step, False)
-
-
-@step('Edit High Level Source is visible')
-def edit_high_level_source_links_visible(step):
- verify_high_level_source_links(step, True)
-
-
-@step('If I press Cancel my changes are not persisted')
-def cancel_does_not_save_changes(step):
- world.cancel_component(step)
- step.given("I edit and select Settings")
- step.given("I see the advanced settings and their expected values")
-
-
-@step('I have enabled latex compiler')
-def enable_latex_compiler(step):
- url = world.browser.url
- step.given("I select the Advanced Settings")
- change_value(step, 'Enable LaTeX Compiler', 'true')
- world.visit(url)
- world.wait_for_xmodule()
-
-
-@step('I have created a LaTeX Problem')
-def create_latex_problem(step):
- step.given('I am in Studio editing a new unit')
- step.given('I have enabled latex compiler')
- world.create_component_instance(
- step=step,
- category='problem',
- component_type='Problem Written in LaTeX',
- is_advanced=True
- )
-
-
-@step('I edit and compile the High Level Source')
-def edit_latex_source(_step):
- open_high_level_source()
- type_in_codemirror(1, "hi")
- world.css_click('.hls-compile')
-
-
-@step('my change to the High Level Source is persisted')
-def high_level_source_persisted(_step):
- def verify_text(_driver):
- css_sel = '.problem div>span'
- return world.css_text(css_sel) == 'hi'
-
- world.wait_for(verify_text, timeout=10)
-
-
-@step('I view the High Level Source I see my changes')
-def high_level_source_in_editor(_step):
- open_high_level_source()
- assert_equal('hi', world.css_value('.source-edit-box'))
-
-
-@step(u'I have an empty course')
-def i_have_empty_course(_step):
- open_new_course()
-
-
-@step(u'I import the file "([^"]*)"$')
-def i_import_the_file(_step, filename):
- import_file(filename)
-
-
-@step(u'I go to the vertical "([^"]*)"$')
-def i_go_to_vertical(_step, vertical):
- world.css_click("span:contains('{0}')".format(vertical))
-
-
-@step(u'I go to the unit "([^"]*)"$')
-def i_go_to_unit(_step, unit):
- loc = "window.location = $(\"span:contains('{0}')\").closest('a').attr('href')".format(unit)
- world.browser.execute_script(loc)
-
-
-@step(u'I see a message that says "([^"]*)"$')
-def i_can_see_message(_step, msg):
- msg = json.dumps(msg) # escape quotes
- world.css_has_text("h2.title", msg)
-
-
-@step(u'I can edit the problem$')
-def i_can_edit_problem(_step):
- world.edit_component()
-
-
-@step(u'I edit first blank advanced problem for annotation response$')
-def i_edit_blank_problem_for_annotation_response(_step):
- world.edit_component(1)
- text = """
-
-
- Text of annotation
-
- """
- type_in_codemirror(0, text)
- world.save_component()
-
-
-@step(u'I can see cheatsheet$')
-def verify_cheat_sheet_displaying(_step):
- world.css_click(".cheatsheet-toggle")
- css_selector = '.simple-editor-cheatsheet'
- world.wait_for_visible(css_selector)
-
-
-def verify_high_level_source_links(step, visible):
- if visible:
- assert_true(world.is_css_present('.launch-latex-compiler'),
- msg="Expected to find the latex button but it is not present.")
- else:
- assert_true(world.is_css_not_present('.launch-latex-compiler'),
- msg="Expected not to find the latex button but it is present.")
-
- world.cancel_component(step)
-
-
-def verify_modified_weight():
- world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "3.5", True)
-
-
-def verify_modified_randomization():
- world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Per Student", True)
-
-
-def verify_modified_display_name():
- world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, '3.4', True)
-
-
-def verify_modified_display_name_with_special_chars():
- world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, "updated ' \" &", True)
-
-
-def verify_modified_display_name_with_html():
- world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME),
- DISPLAY_NAME, "", True)
-
-
-def verify_unset_display_name():
- world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, 'Blank Advanced Problem', False)
-
-
-def set_weight(weight):
- index = world.get_setting_entry_index(PROBLEM_WEIGHT)
- world.set_field_value(index, weight)
-
-
-def open_high_level_source():
- world.edit_component()
- world.css_click('.launch-latex-compiler > a')
diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py
deleted file mode 100644
index e0b2ca9349..0000000000
--- a/cms/djangoapps/contentstore/features/signup.py
+++ /dev/null
@@ -1,71 +0,0 @@
-# pylint: disable=missing-docstring
-# pylint: disable=no-member
-
-from lettuce import step, world
-
-
-@step('I fill in the registration form$')
-def i_fill_in_the_registration_form(_step):
- def fill_in_reg_form():
- register_form = world.css_find('form#register_form')
- register_form.find_by_name('email').fill('robot+studio@edx.org')
- register_form.find_by_name('password').fill('test')
- register_form.find_by_name('username').fill('robot-studio')
- register_form.find_by_name('name').fill('Robot Studio')
- register_form.find_by_name('terms_of_service').click()
- world.retry_on_exception(fill_in_reg_form)
-
-
-@step('I press the Create My Account button on the registration form$')
-def i_press_the_button_on_the_registration_form(_step):
- submit_css = 'form#register_form button#submit'
- world.css_click(submit_css)
-
-
-@step('I should see an email verification prompt')
-def i_should_see_an_email_verification_prompt(_step):
- world.css_has_text('h1.page-header', u'Studio Home')
- world.css_has_text('div.msg h3.title', u'We need to verify your email address')
-
-
-@step(u'I fill in and submit the signin form$')
-def i_fill_in_the_signin_form(_step):
- def fill_login_form():
- login_form = world.browser.find_by_css('form#login_form')
- login_form.find_by_name('email').fill('robot+studio@edx.org')
- login_form.find_by_name('password').fill('test')
- login_form.find_by_name('submit').click()
- world.retry_on_exception(fill_login_form)
-
-
-@step(u'I should( not)? see a login error message$')
-def i_should_see_a_login_error(_step, should_not_see):
- if should_not_see:
- # the login error may be absent or invisible. Check absence first,
- # because css_visible will throw an exception if the element is not present
- if world.is_css_present('div#login_error'):
- assert not world.css_visible('div#login_error')
- else:
- assert world.css_visible('div#login_error')
-
-
-@step(u'I fill in and submit the signin form incorrectly$')
-def i_goof_in_the_signin_form(_step):
- def fill_login_form():
- login_form = world.browser.find_by_css('form#login_form')
- login_form.find_by_name('email').fill('robot+studio@edx.org')
- login_form.find_by_name('password').fill('oops')
- login_form.find_by_name('submit').click()
- world.retry_on_exception(fill_login_form)
-
-
-@step(u'I edit the password field$')
-def i_edit_the_password_field(_step):
- password_css = 'form#login_form input#password'
- world.css_fill(password_css, 'test')
-
-
-@step(u'I submit the signin form$')
-def i_submit_the_signin_form(_step):
- submit_css = 'form#login_form button#submit'
- world.css_click(submit_css)
diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py
deleted file mode 100644
index a37cf8d170..0000000000
--- a/cms/envs/acceptance.py
+++ /dev/null
@@ -1,149 +0,0 @@
-"""
-This config file extends the test environment configuration
-so that we can run the lettuce acceptance tests.
-"""
-
-# We intentionally define lots of variables that aren't used, and
-# want to import all variables from base settings files
-# pylint: disable=wildcard-import, unused-wildcard-import
-
-from .test import *
-
-# You need to start the server in debug mode,
-# otherwise the browser will not render the pages correctly
-DEBUG = True
-
-# Output Django logs to a file
-import logging
-logging.basicConfig(filename=TEST_ROOT / "log" / "cms_acceptance.log", level=logging.ERROR)
-
-# set root logger level
-logging.getLogger().setLevel(logging.ERROR)
-
-import os
-
-
-def seed():
- return os.getppid()
-
-# Silence noisy logs
-LOG_OVERRIDES = [
- ('track.middleware', logging.CRITICAL),
- ('codejail.safe_exec', logging.ERROR),
- ('edx.courseware', logging.ERROR),
- ('edxmako.shortcuts', logging.ERROR),
- ('audit', logging.ERROR),
- ('contentstore.views.import_export', logging.CRITICAL),
- ('xmodule.x_module', logging.CRITICAL),
-]
-
-for log_name, log_level in LOG_OVERRIDES:
- logging.getLogger(log_name).setLevel(log_level)
-
-update_module_store_settings(
- MODULESTORE,
- doc_store_settings={
- 'db': 'acceptance_xmodule',
- 'collection': 'acceptance_modulestore_%s' % seed(),
- },
- module_store_options={
- 'default_class': 'xmodule.raw_module.RawDescriptor',
- 'fs_root': TEST_ROOT / "data",
- },
- default_store=os.environ.get('DEFAULT_STORE', 'draft'),
-)
-
-CONTENTSTORE = {
- 'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
- 'DOC_STORE_CONFIG': {
- 'host': 'localhost',
- 'db': 'acceptance_xcontent_%s' % seed(),
- },
- # allow for additional options that can be keyed on a name, e.g. 'trashcan'
- 'ADDITIONAL_OPTIONS': {
- 'trashcan': {
- 'bucket': 'trash_fs'
- }
- }
-}
-
-# Set this up so that 'paver cms --settings=acceptance' and running the
-# harvest command both use the same (test) database
-# which they can flush without messing up your dev db
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': TEST_ROOT / "db" / "test_edx.db",
- 'OPTIONS': {
- 'timeout': 30,
- },
- 'ATOMIC_REQUESTS': True,
- 'TEST': {
- 'NAME': TEST_ROOT / "db" / "test_edx.db",
- },
- },
- 'student_module_history': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': TEST_ROOT / "db" / "test_student_module_history.db",
- 'OPTIONS': {
- 'timeout': 30,
- },
- 'TEST': {
- 'NAME': TEST_ROOT / "db" / "test_student_module_history.db",
- },
- }
-}
-
-# Use the auto_auth workflow for creating users and logging them in
-FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True
-FEATURES['RESTRICT_AUTOMATIC_AUTH'] = False
-
-# Forums are disabled in test.py to speed up unit tests, but we do not have
-# per-test control for lettuce acceptance tests.
-# If you are writing an acceptance test that needs the discussion service enabled,
-# do not write it in lettuce, but instead write it using bok-choy.
-# DO NOT CHANGE THIS SETTING HERE.
-FEATURES['ENABLE_DISCUSSION_SERVICE'] = False
-
-# HACK
-# Setting this flag to false causes imports to not load correctly in the lettuce python files
-# We do not yet understand why this occurs. Setting this to true is a stopgap measure
-USE_I18N = True
-
-# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command
-# django.contrib.staticfiles used to be loaded by lettuce, now we must add it ourselves
-# django.contrib.staticfiles is not added to lms as there is a ^/static$ route built in to the app
-INSTALLED_APPS.append('lettuce.django')
-LETTUCE_APPS = ('contentstore',)
-LETTUCE_BROWSER = os.environ.get('LETTUCE_BROWSER', 'chrome')
-
-# Where to run: local, saucelabs, or grid
-LETTUCE_SELENIUM_CLIENT = os.environ.get('LETTUCE_SELENIUM_CLIENT', 'local')
-
-SELENIUM_GRID = {
- 'URL': 'http://127.0.0.1:4444/wd/hub',
- 'BROWSER': LETTUCE_BROWSER,
-}
-
-#####################################################################
-# Lastly, see if the developer has any local overrides.
-try:
- from .private import *
-except ImportError:
- pass
-
-# Point the URL used to test YouTube availability to our stub YouTube server
-YOUTUBE_HOSTNAME = os.environ.get('BOK_CHOY_HOSTNAME', '127.0.0.1')
-YOUTUBE['API'] = "http://{0}:{1}/get_youtube_api/".format(YOUTUBE_HOSTNAME, YOUTUBE_PORT)
-YOUTUBE['METADATA_URL'] = "http://{0}:{1}/test_youtube/".format(YOUTUBE_HOSTNAME, YOUTUBE_PORT)
-YOUTUBE['TEXT_API']['url'] = "{0}:{1}/test_transcripts_youtube/".format(YOUTUBE_HOSTNAME, YOUTUBE_PORT)
-YOUTUBE['TEST_TIMEOUT'] = 1500
-
-# Generate a random UUID so that different runs of acceptance tests don't break each other
-import uuid
-SECRET_KEY = uuid.uuid4().hex
-
-############################### PIPELINE #######################################
-
-PIPELINE_ENABLED = False
-REQUIRE_DEBUG = True
diff --git a/cms/envs/acceptance_docker.py b/cms/envs/acceptance_docker.py
deleted file mode 100644
index b00a94db5f..0000000000
--- a/cms/envs/acceptance_docker.py
+++ /dev/null
@@ -1,61 +0,0 @@
-"""
-This config file extends the test environment configuration
-so that we can run the lettuce acceptance tests.
-"""
-
-# We intentionally define lots of variables that aren't used, and
-# want to import all variables from base settings files
-# pylint: disable=wildcard-import, unused-wildcard-import
-
-import os
-
-os.environ['EDXAPP_TEST_MONGO_HOST'] = os.environ.get('EDXAPP_TEST_MONGO_HOST', 'edx.devstack.mongo')
-
-# noinspection PyUnresolvedReferences
-from .acceptance import *
-
-update_module_store_settings(
- MODULESTORE,
- doc_store_settings={
- 'db': 'acceptance_xmodule',
- 'host': MONGO_HOST,
- 'port': MONGO_PORT_NUM,
- 'collection': 'acceptance_modulestore_%s' % seed(),
- },
- module_store_options={
- 'default_class': 'xmodule.raw_module.RawDescriptor',
- 'fs_root': TEST_ROOT / "data",
- },
- default_store=os.environ.get('DEFAULT_STORE', 'draft'),
-)
-
-CONTENTSTORE = {
- 'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
- 'DOC_STORE_CONFIG': {
- 'host': MONGO_HOST,
- 'port': MONGO_PORT_NUM,
- 'db': 'acceptance_xcontent_%s' % seed(),
- },
- # allow for additional options that can be keyed on a name, e.g. 'trashcan'
- 'ADDITIONAL_OPTIONS': {
- 'trashcan': {
- 'bucket': 'trash_fs'
- }
- }
-}
-
-# Where to run: local, saucelabs, or grid
-LETTUCE_SELENIUM_CLIENT = os.environ.get('LETTUCE_SELENIUM_CLIENT', 'grid')
-SELENIUM_HOST = 'edx.devstack.{}'.format(LETTUCE_BROWSER)
-SELENIUM_PORT = os.environ.get('SELENIUM_PORT', '4444')
-
-SELENIUM_GRID = {
- 'URL': 'http://{}:{}/wd/hub'.format(SELENIUM_HOST, SELENIUM_PORT),
- 'BROWSER': LETTUCE_BROWSER,
-}
-
-# Point the URL used to test YouTube availability to our stub YouTube server
-LETTUCE_HOST = os.environ['BOK_CHOY_HOSTNAME']
-YOUTUBE['API'] = "http://{}:{}/get_youtube_api/".format(LETTUCE_HOST, YOUTUBE_PORT)
-YOUTUBE['METADATA_URL'] = "http://{}:{}/test_youtube/".format(LETTUCE_HOST, YOUTUBE_PORT)
-YOUTUBE['TEXT_API']['url'] = "{}:{}/test_transcripts_youtube/".format(LETTUCE_HOST, YOUTUBE_PORT)
diff --git a/cms/envs/test.py b/cms/envs/test.py
index a4592d6a7b..6b64990c9f 100644
--- a/cms/envs/test.py
+++ b/cms/envs/test.py
@@ -185,7 +185,6 @@ CLEAR_REQUEST_CACHE_ON_TASK_COMPLETION = False
# These ports are carefully chosen so that if the browser needs to
# access them, they will be available through the SauceLabs SSH tunnel
-LETTUCE_SERVER_PORT = 8003
XQUEUE_PORT = 8040
YOUTUBE_PORT = 8031
LTI_PORT = 8765
diff --git a/common/djangoapps/terrain/__init__.py b/common/djangoapps/terrain/__init__.py
index b769c8414c..e69de29bb2 100644
--- a/common/djangoapps/terrain/__init__.py
+++ b/common/djangoapps/terrain/__init__.py
@@ -1,14 +0,0 @@
-# Use this as your lettuce terrain file so that the common steps
-# across all lms apps can be put in terrain/common
-# See https://groups.google.com/forum/?fromgroups=#!msg/lettuce-users/5VyU9B4HcX8/USgbGIJdS5QJ
-
-import lettuce
-from django.utils.functional import SimpleLazyObject
-from .browser import * # pylint: disable=wildcard-import
-from .factories import absorb_factories
-from .steps import * # pylint: disable=wildcard-import
-from .setup_prereqs import * # pylint: disable=wildcard-import
-
-# Delay absorption of factories until the next access,
-# after Django apps have finished initializing
-setattr(lettuce, 'world', SimpleLazyObject(absorb_factories))
diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py
deleted file mode 100644
index 455ba2cf25..0000000000
--- a/common/djangoapps/terrain/browser.py
+++ /dev/null
@@ -1,288 +0,0 @@
-"""
-Browser set up for acceptance tests.
-"""
-
-# pylint: disable=no-member
-# pylint: disable=unused-argument
-
-from base64 import encodestring
-from json import dumps
-from logging import getLogger
-
-import requests
-from django.conf import settings
-from django.core.management import call_command
-from lettuce import after, before, world
-from selenium.common.exceptions import WebDriverException
-from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
-from splinter.browser import Browser
-
-from xmodule.contentstore.django import _CONTENTSTORE
-
-LOGGER = getLogger(__name__)
-LOGGER.info("Loading the lettuce acceptance testing terrain file...")
-
-MAX_VALID_BROWSER_ATTEMPTS = 20
-GLOBAL_SCRIPT_TIMEOUT = 60
-
-
-def get_saucelabs_username_and_key():
- """
- Returns the Sauce Labs username and access ID as set by environment variables
- """
- return {"username": settings.SAUCE.get('USERNAME'), "access-key": settings.SAUCE.get('ACCESS_ID')}
-
-
-def set_saucelabs_job_status(jobid, passed=True):
- """
- Sets the job status on sauce labs
- """
- config = get_saucelabs_username_and_key()
- url = 'http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], world.jobid)
- body_content = dumps({"passed": passed})
- base64string = encodestring('{}:{}'.format(config['username'], config['access-key']))[:-1]
- headers = {"Authorization": "Basic {}".format(base64string)}
- result = requests.put(url, data=body_content, headers=headers)
- return result.status_code == 200
-
-
-def make_saucelabs_desired_capabilities():
- """
- Returns a DesiredCapabilities object corresponding to the environment sauce parameters
- """
- desired_capabilities = settings.SAUCE.get('BROWSER', DesiredCapabilities.CHROME)
- desired_capabilities['platform'] = settings.SAUCE.get('PLATFORM')
- desired_capabilities['version'] = settings.SAUCE.get('VERSION')
- desired_capabilities['device-type'] = settings.SAUCE.get('DEVICE')
- desired_capabilities['name'] = settings.SAUCE.get('SESSION')
- desired_capabilities['build'] = settings.SAUCE.get('BUILD')
- desired_capabilities['video-upload-on-pass'] = False
- desired_capabilities['sauce-advisor'] = False
- desired_capabilities['capture-html'] = True
- desired_capabilities['record-screenshots'] = True
- desired_capabilities['selenium-version'] = "2.34.0"
- desired_capabilities['max-duration'] = 3600
- desired_capabilities['public'] = 'public restricted'
- return desired_capabilities
-
-
-@before.harvest
-def initial_setup(server):
- """
- Launch the browser once before executing the tests.
- """
- world.absorb(settings.LETTUCE_SELENIUM_CLIENT, 'LETTUCE_SELENIUM_CLIENT')
-
- if world.LETTUCE_SELENIUM_CLIENT == 'local':
- browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome')
-
- if browser_driver == 'chrome':
- desired_capabilities = DesiredCapabilities.CHROME
- desired_capabilities['loggingPrefs'] = {
- 'browser': 'ALL',
- }
- else:
- desired_capabilities = {}
-
- # There is an issue with ChromeDriver2 r195627 on Ubuntu
- # in which we sometimes get an invalid browser session.
- # This is a work-around to ensure that we get a valid session.
- success = False
- num_attempts = 0
- while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS:
-
- # Load the browser and try to visit the main page
- # If the browser couldn't be reached or
- # the browser session is invalid, this will
- # raise a WebDriverException
- try:
- if browser_driver == 'firefox':
- # Lettuce initializes differently for firefox, and sending
- # desired_capabilities will not work. So initialize without
- # sending desired_capabilities.
- world.browser = Browser(browser_driver)
- else:
- world.browser = Browser(browser_driver, desired_capabilities=desired_capabilities)
- world.browser.driver.set_script_timeout(GLOBAL_SCRIPT_TIMEOUT)
- world.visit('/')
-
- except WebDriverException:
- LOGGER.warn("Error acquiring %s browser, retrying", browser_driver, exc_info=True)
- if hasattr(world, 'browser'):
- world.browser.quit()
- num_attempts += 1
-
- else:
- success = True
-
- # If we were unable to get a valid session within the limit of attempts,
- # then we cannot run the tests.
- if not success:
- raise IOError("Could not acquire valid {driver} browser session.".format(driver=browser_driver))
-
- world.absorb(0, 'IMPLICIT_WAIT')
- world.browser.driver.set_window_size(1280, 1024)
-
- elif world.LETTUCE_SELENIUM_CLIENT == 'saucelabs':
- config = get_saucelabs_username_and_key()
- world.browser = Browser(
- 'remote',
- url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'], config['access-key']),
- **make_saucelabs_desired_capabilities()
- )
- world.absorb(30, 'IMPLICIT_WAIT')
- world.browser.set_script_timeout(GLOBAL_SCRIPT_TIMEOUT)
-
- elif world.LETTUCE_SELENIUM_CLIENT == 'grid':
- world.browser = Browser(
- 'remote',
- url=settings.SELENIUM_GRID.get('URL'),
- browser=settings.SELENIUM_GRID.get('BROWSER'),
- )
- world.absorb(30, 'IMPLICIT_WAIT')
- world.browser.driver.set_script_timeout(GLOBAL_SCRIPT_TIMEOUT)
-
- else:
- raise Exception("Unknown selenium client '{}'".format(world.LETTUCE_SELENIUM_CLIENT))
-
- world.browser.driver.implicitly_wait(world.IMPLICIT_WAIT)
- world.absorb(world.browser.driver.session_id, 'jobid')
-
-
-@before.each_scenario
-def reset_data(scenario):
- """
- Clean out the django test database defined in the
- envs/acceptance.py file: edx-platform/db/test_edx.db
- """
- LOGGER.debug("Flushing the test database...")
- call_command('flush', interactive=False, verbosity=0)
- world.absorb({}, 'scenario_dict')
-
-
-@before.each_scenario
-def configure_screenshots(scenario):
- """
- Before each scenario, turn off automatic screenshots.
-
- Args: str, scenario. Name of current scenario.
- """
- world.auto_capture_screenshots = False
-
-
-@after.each_scenario
-def clear_data(scenario):
- world.spew('scenario_dict')
-
-
-@after.each_scenario
-def reset_databases(scenario):
- """
- After each scenario, all databases are cleared/dropped. Contentstore data are stored in unique databases
- whereas modulestore data is in unique collection names. This data is created implicitly during the scenarios.
- If no data is created during the test, these lines equivilently do nothing.
- """
- import xmodule.modulestore.django
- xmodule.modulestore.django.modulestore()._drop_database() # pylint: disable=protected-access
- xmodule.modulestore.django.clear_existing_modulestores()
- _CONTENTSTORE.clear()
-
-
-@world.absorb
-def capture_screenshot(image_name):
- """
- Capture a screenshot outputting it to a defined directory.
- This function expects only the name of the file. It will generate
- the full path of the output screenshot.
-
- If the name contains spaces, they ill be converted to underscores.
- """
- output_dir = '{}/log/auto_screenshots'.format(settings.TEST_ROOT)
- image_name = '{}/{}.png'.format(output_dir, image_name.replace(' ', '_'))
- try:
- world.browser.driver.save_screenshot(image_name)
- except WebDriverException:
- LOGGER.error("Could not capture a screenshot '{}'".format(image_name))
-
-
-@after.each_scenario
-def screenshot_on_error(scenario):
- """
- Save a screenshot to help with debugging.
- """
- if scenario.failed:
- try:
- output_dir = '{}/log'.format(settings.TEST_ROOT)
- image_name = '{}/{}.png'.format(output_dir, scenario.name.replace(' ', '_'))
- world.browser.driver.save_screenshot(image_name)
- except WebDriverException:
- LOGGER.error('Could not capture a screenshot')
-
-
-@after.each_scenario
-def capture_console_log(scenario):
- """
- Save the console log to help with debugging.
- """
- if scenario.failed:
- log = world.browser.driver.get_log('browser')
- try:
- output_dir = '{}/log'.format(settings.TEST_ROOT)
- file_name = '{}/{}.log'.format(output_dir, scenario.name.replace(' ', '_'))
-
- with open(file_name, 'w') as output_file:
- for line in log:
- output_file.write("{}{}".format(dumps(line), '\n'))
-
- except WebDriverException:
- LOGGER.error('Could not capture the console log')
-
-
-def capture_screenshot_for_step(step, when):
- """
- Useful method for debugging acceptance tests that are run in Vagrant.
- This method runs automatically before and after each step of an acceptance
- test scenario. The variable:
-
- world.auto_capture_screenshots
-
- either enables or disabled the taking of screenshots. To change the
- variable there is a convenient step defined:
-
- I (enable|disable) auto screenshots
-
- If you just want to capture a single screenshot at a desired point in code,
- you should use the method:
-
- world.capture_screenshot("image_name")
- """
- if world.auto_capture_screenshots:
- scenario_num = step.scenario.feature.scenarios.index(step.scenario) + 1
- step_num = step.scenario.steps.index(step) + 1
- step_func_name = step.defined_at.function.func_name
- image_name = "{prefix:03d}__{num:03d}__{name}__{postfix}".format(
- prefix=scenario_num,
- num=step_num,
- name=step_func_name,
- postfix=when
- )
- world.capture_screenshot(image_name)
-
-
-@before.each_step
-def before_each_step(step):
- capture_screenshot_for_step(step, '1_before')
-
-
-@after.each_step
-def after_each_step(step):
- capture_screenshot_for_step(step, '2_after')
-
-
-@after.harvest
-def saucelabs_status(total):
- """
- Collect data for saucelabs.
- """
- if world.LETTUCE_SELENIUM_CLIENT == 'saucelabs':
- set_saucelabs_job_status(world.jobid, total.scenarios_ran == total.scenarios_passed)
diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py
deleted file mode 100644
index 94457a44fd..0000000000
--- a/common/djangoapps/terrain/course_helpers.py
+++ /dev/null
@@ -1,77 +0,0 @@
-# pylint: disable=missing-docstring
-
-import urllib
-
-from django.apps import apps
-from django.contrib.auth import get_user_model
-from lettuce import world
-
-from xmodule.contentstore.django import _CONTENTSTORE
-
-
-@world.absorb
-def create_user(uname, password):
-
- # If the user already exists, don't try to create it again
- if len(get_user_model().objects.filter(username=uname)) > 0:
- return
-
- portal_user = world.UserFactory.build(username=uname, email=uname + '@edx.org')
- portal_user.set_password(password)
- portal_user.save()
-
- registration = world.RegistrationFactory(user=portal_user)
- registration.register(portal_user)
- registration.activate()
-
- world.UserProfileFactory(user=portal_user)
-
-
-@world.absorb
-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'
- 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
- user = get_user_model().objects.get(username=username)
- world.scenario_dict['USER'] = user
-
-
-@world.absorb
-def register_by_course_key(course_key, username='robot', password='test', is_staff=False):
- create_user(username, password)
- user = get_user_model().objects.get(username=username)
- # Note: this flag makes the user global staff - that is, an edX employee - not a course staff.
- # See courseware.tests.factories for StaffFactory and InstructorFactory.
- if is_staff:
- user.is_staff = True
- user.save()
- apps.get_model('student', 'CourseEnrollment').enroll(user, course_key)
-
-
-@world.absorb
-def enroll_user(user, course_key):
- # Activate user
- registration = world.RegistrationFactory(user=user)
- registration.register(user)
- registration.activate()
- # Enroll them in the course
- apps.get_model('student', 'CourseEnrollment').enroll(user, course_key)
-
-
-@world.absorb
-def clear_courses():
- # Flush and initialize the module store
- # Note that if your test module gets in some weird state
- # (though it shouldn't), do this manually
- # from the bash shell to drop it:
- # $ mongo test_xmodule --eval "db.dropDatabase()"
- from xmodule.modulestore.django import clear_existing_modulestores, modulestore
- modulestore()._drop_database() # pylint: disable=protected-access
- _CONTENTSTORE.clear()
- clear_existing_modulestores()
diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py
deleted file mode 100644
index 8df3f354d8..0000000000
--- a/common/djangoapps/terrain/factories.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""
-Factories are defined in other modules and absorbed here into the
-lettuce world so that they can be used by both unit tests
-and integration / BDD tests.
-"""
-from lettuce import world
-
-
-def absorb_factories():
- """
- Absorb the factories and return the resulting ``world`` object.
- """
- import course_modes.tests.factories as cmf
- import student.tests.factories as sf
- import xmodule.modulestore.tests.factories as xf
-
- # Unlock XBlock factories, because we're randomizing the collection
- # name above to prevent collisions
- xf.XMODULE_FACTORY_LOCK.enable()
-
- world.absorb(sf.UserFactory)
- world.absorb(sf.UserProfileFactory)
- world.absorb(sf.RegistrationFactory)
- world.absorb(sf.GroupFactory)
- world.absorb(sf.CourseEnrollmentAllowedFactory)
- world.absorb(cmf.CourseModeFactory)
- world.absorb(xf.CourseFactory)
- world.absorb(xf.ItemFactory)
-
- return world
diff --git a/common/djangoapps/terrain/setup_prereqs.py b/common/djangoapps/terrain/setup_prereqs.py
deleted file mode 100644
index 62ade03575..0000000000
--- a/common/djangoapps/terrain/setup_prereqs.py
+++ /dev/null
@@ -1,162 +0,0 @@
-"""
-Set up the prequisites for acceptance tests.
-
-This includes initialization and teardown for stub and video HTTP services
-and checking for external URLs that need to be accessible and responding.
-
-"""
-import re
-from logging import getLogger
-
-import requests
-from django.conf import settings
-from lettuce import after, before, world
-from selenium.common.exceptions import NoAlertPresentException
-
-from terrain.stubs.lti import StubLtiService
-from terrain.stubs.video_source import VideoSourceHttpService
-from terrain.stubs.xqueue import StubXQueueService
-from terrain.stubs.youtube import StubYouTubeService
-
-LOGGER = getLogger(__name__)
-
-SERVICES = {
- "youtube": {"port": settings.YOUTUBE_PORT, "class": StubYouTubeService},
- "xqueue": {"port": settings.XQUEUE_PORT, "class": StubXQueueService},
- "lti": {"port": settings.LTI_PORT, "class": StubLtiService},
-}
-
-YOUTUBE_API_URLS = {
- 'main': 'https://www.youtube.com/',
- 'player': 'https://www.youtube.com/iframe_api',
- # For transcripts, you need to check an actual video, so we will
- # just specify our default video and see if that one is available.
- 'transcript': 'http://video.google.com/timedtext?lang=en&v=OEoXaMPEzfM',
-}
-
-
-@before.all # pylint: disable=no-member
-def start_video_server():
- """
- Serve the HTML5 Video Sources from a local port
- """
- video_source_dir = '{}/data/video'.format(settings.TEST_ROOT)
- video_server = VideoSourceHttpService(port_num=settings.VIDEO_SOURCE_PORT)
- video_server.config['root_dir'] = video_source_dir
- world.video_source = video_server
-
-
-@after.all # pylint: disable=no-member
-def stop_video_server(_total):
- """
- Stop the HTML5 Video Source server after all tests have executed
- """
- video_server = getattr(world, 'video_source', None)
- if video_server:
- video_server.shutdown()
-
-
-@before.all # pylint: disable=no-member
-def start_stub_servers():
- """
- Start all stub servers
- """
-
- for stub in SERVICES.keys():
- start_stub(stub)
-
-
-@before.each_scenario # pylint: disable=no-member
-def skip_youtube_if_not_available(scenario):
- """
-
- Scenario tags must be named with this convention:
- @requires_stub_bar, where 'bar' is the name of the stub service to start
-
- if 'bar' is 'youtube'
- if 'youtube' is not available Then
- DON'T start youtube stub server
- ALSO DON'T start any other stub server BECAUSE we will SKIP this Scenario so no need to start any stub
- else
- start the stub server
-
- """
- tag_re = re.compile('requires_stub_(?P[^_]+)')
- for tag in scenario.tags:
- requires = tag_re.match(tag)
-
- if requires:
- if requires.group('server') == 'youtube':
- if not is_youtube_available(YOUTUBE_API_URLS):
- # A hackish way to skip a test in lettuce as there is no proper way to skip a test conditionally
- scenario.steps = []
- return
-
- return
-
-
-def start_stub(name):
- """
- Start the required stub service running on a local port.
- Since these services can be reconfigured on the fly,
- we start them on a scenario basis when needed and
- stop them at the end of the scenario.
- """
- service = SERVICES.get(name, None)
- if service:
- fake_server = service['class'](port_num=service['port'])
- setattr(world, name, fake_server)
-
-
-def is_youtube_available(urls):
- """
- Check if the required youtube urls are available.
- If they are not, then skip the scenario.
- """
- for name, url in urls.iteritems():
- try:
- response = requests.get(url, allow_redirects=False)
- except requests.exceptions.ConnectionError:
- LOGGER.warning("Connection Error. YouTube {0} service not available. Skipping this test.".format(name))
- return False
-
- status = response.status_code
- if status >= 300:
- LOGGER.warning(
- "YouTube {0} service not available. Status code: {1}. Skipping this test.".format(name, status))
-
- # No need to check all the URLs
- return False
-
- return True
-
-
-@after.all # pylint: disable=no-member
-def stop_stubs(_scenario):
- """
- Shut down any stub services.
- """
- # close browser to ensure no open connections to the stub servers
- world.browser.quit()
- for name in SERVICES.keys():
- stub_server = getattr(world, name, None)
- if stub_server is not None:
- stub_server.shutdown()
-
-
-@after.each_scenario # pylint: disable=no-member
-def clear_alerts(_scenario):
- """
- Clear any alerts that might still exist, so that
- the next scenario will not fail due to their existence.
-
- Note that the splinter documentation indicates that
- get_alert should return None if no alert is present,
- however that is not the case. Instead a
- NoAlertPresentException is raised.
- """
- try:
- with world.browser.get_alert() as alert:
- alert.dismiss()
- except NoAlertPresentException:
- pass
diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py
deleted file mode 100644
index e4114ff6b9..0000000000
--- a/common/djangoapps/terrain/steps.py
+++ /dev/null
@@ -1,244 +0,0 @@
-# pylint: disable=missing-docstring
-# pylint: disable=redefined-outer-name
-
-# Disable the "wildcard import" warning so we can bring in all methods from
-# course helpers and ui helpers
-# pylint: disable=wildcard-import
-
-# Disable the "Unused import %s from wildcard import" warning
-# pylint: disable=unused-wildcard-import
-
-# Disable the "unused argument" warning because lettuce uses "step"
-# pylint: disable=unused-argument
-
-# django_url is assigned late in the process of loading lettuce,
-from logging import getLogger
-
-# so we import this as a module, and then read django_url from
-# it to get the correct value
-import lettuce.django
-from lettuce import step, world
-from opaque_keys.edx.keys import CourseKey
-
-from openedx.core.lib.tests.tools import assert_equals # pylint: disable=no-name-in-module
-
-from .course_helpers import *
-from .ui_helpers import *
-
-logger = getLogger(__name__)
-
-
-@step(r'I wait (?:for )?"(\d+\.?\d*)" seconds?$')
-def wait_for_seconds(step, seconds):
- world.wait(seconds)
-
-
-@step('I reload the page$')
-def reload_the_page(step):
- world.wait_for_ajax_complete()
- world.browser.reload()
- world.wait_for_js_to_load()
-
-
-@step('I press the browser back button$')
-def browser_back(step):
- world.browser.driver.back()
-
-
-@step('I (?:visit|access|open) the homepage$')
-def i_visit_the_homepage(step):
- world.visit('/')
- assert world.is_css_present('header.global')
-
-
-@step(u'I (?:visit|access|open) the dashboard$')
-def i_visit_the_dashboard(step):
- world.visit('/dashboard')
- assert world.is_css_present('.dashboard')
-
-
-@step('I should be on the dashboard page$')
-def i_should_be_on_the_dashboard(step):
- assert world.is_css_present('.dashboard')
- assert 'Dashboard' in world.browser.title
-
-
-@step(u'I (?:visit|access|open) the courses page$')
-def i_am_on_the_courses_page(step):
- world.visit('/courses')
- assert world.is_css_present('div.courses')
-
-
-@step(u'I press the "([^"]*)" button$')
-def and_i_press_the_button(step, value):
- button_css = 'input[value="%s"]' % value
- world.css_click(button_css)
-
-
-@step(u'I click the link with the text "([^"]*)"$')
-def click_the_link_with_the_text_group1(step, linktext):
- world.click_link(linktext)
-
-
-@step('I should see that the path is "([^"]*)"$')
-def i_should_see_that_the_path_is(step, path):
- if 'COURSE' in world.scenario_dict:
- path = path.format(world.scenario_dict['COURSE'].id)
- assert world.url_equals(path), (
- "path should be {!r} but is {!r}".format(path, world.browser.url)
- )
-
-
-@step(u'the page title should be "([^"]*)"$')
-def the_page_title_should_be(step, title):
- assert_equals(world.browser.title, title)
-
-
-@step(u'the page title should contain "([^"]*)"$')
-def the_page_title_should_contain(step, title):
- assert title in world.browser.title
-
-
-@step('I log in$')
-def i_log_in(step):
- world.log_in(username='robot', password='test')
-
-
-@step('I am a logged in user$')
-def i_am_logged_in_user(step):
- world.create_user('robot', 'test')
- world.log_in(username='robot', password='test')
-
-
-@step('I am not logged in$')
-def i_am_not_logged_in(step):
- world.visit('logout')
-
-
-@step('I am staff for course "([^"]*)"$')
-def i_am_staff_for_course_by_id(step, course_id):
- course_key = CourseKey.from_string(course_id)
- world.register_by_course_key(course_key, True)
-
-
-@step(r'click (?:the|a) link (?:called|with the text) "([^"]*)"$')
-def click_the_link_called(step, text):
- world.click_link(text)
-
-
-@step(r'should see that the url is "([^"]*)"$')
-def should_have_the_url(step, url):
- assert_equals(world.browser.url, url)
-
-
-@step(r'should see (?:the|a) link (?:called|with the text) "([^"]*)"$')
-def should_see_a_link_called(step, text):
- assert len(world.browser.find_link_by_text(text)) > 0
-
-
-@step(r'should see (?:the|a) link with the id "([^"]*)" called "([^"]*)"$')
-def should_have_link_with_id_and_text(step, link_id, text):
- link = world.browser.find_by_id(link_id)
- assert len(link) > 0
- assert_equals(link.text, text)
-
-
-@step(r'should see a link to "([^"]*)" with the text "([^"]*)"$')
-def should_have_link_with_path_and_text(step, path, text):
- link = world.browser.find_link_by_text(text)
- assert len(link) > 0
- assert_equals(link.first["href"], lettuce.django.django_url(path))
-
-
-@step(r'should( not)? see "(.*)" (?:somewhere|anywhere) (?:in|on) (?:the|this) page')
-def should_see_in_the_page(step, doesnt_appear, text):
- if world.LETTUCE_SELENIUM_CLIENT == 'saucelabs':
- multiplier = 2
- else:
- multiplier = 1
- if doesnt_appear:
- assert world.browser.is_text_not_present(text, wait_time=5 * multiplier)
- else:
- assert world.browser.is_text_present(text, wait_time=5 * multiplier)
-
-
-@step('I am logged in$')
-def i_am_logged_in(step):
- world.create_user('robot', 'test')
- world.log_in(username='robot', password='test')
- world.browser.visit(lettuce.django.django_url('/'))
- dash_css = '.dashboard'
- assert world.is_css_present(dash_css)
-
-
-@step(u'I am an edX user$')
-def i_am_an_edx_user(step):
- world.create_user('robot', 'test')
-
-
-@step(u'User "([^"]*)" is an edX user$')
-def registered_edx_user(step, uname):
- world.create_user(uname, 'test')
-
-
-@step(u'All dialogs should be closed$')
-def dialogs_are_closed(step):
- assert world.dialogs_closed()
-
-
-@step(u'visit the url "([^"]*)"')
-def visit_url(step, url):
- if 'COURSE' in world.scenario_dict:
- url = url.format(world.scenario_dict['COURSE'].id)
- world.browser.visit(lettuce.django.django_url(url))
-
-
-@step(u'wait for AJAX to (?:finish|complete)')
-def wait_ajax(_step):
- wait_for_ajax_complete()
-
-
-@step('I will confirm all alerts')
-def i_confirm_all_alerts(step):
- """
- Please note: This method must be called RIGHT BEFORE an expected alert
- Window variables are page local and thus all changes are removed upon navigating to a new page
- In addition, this method changes the functionality of ONLY future alerts
- """
- world.browser.execute_script('window.confirm = function(){return true;} ; window.alert = function(){return;}')
-
-
-@step('I will cancel all alerts')
-def i_cancel_all_alerts(step):
- """
- Please note: This method must be called RIGHT BEFORE an expected alert
- Window variables are page local and thus all changes are removed upon navigating to a new page
- In addition, this method changes the functionality of ONLY future alerts
- """
- world.browser.execute_script('window.confirm = function(){return false;} ; window.alert = function(){return;}')
-
-
-@step('I will answer all prompts with "([^"]*)"')
-def i_answer_prompts_with(step, prompt):
- """
- Please note: This method must be called RIGHT BEFORE an expected alert
- Window variables are page local and thus all changes are removed upon navigating to a new page
- In addition, this method changes the functionality of ONLY future alerts
- """
- world.browser.execute_script('window.prompt = function(){return %s;}') % prompt
-
-
-@step('I run ipdb')
-def run_ipdb(_step):
- """Run ipdb as step for easy debugging"""
- import ipdb
- ipdb.set_trace()
- assert True
-
-
-@step(u'(I am viewing|s?he views) the course team settings$')
-def view_course_team_settings(_step, whom):
- """ navigates to course team settings page """
- world.click_course_settings()
- link_css = 'li.nav-course-settings-team a'
- world.css_click(link_css)
diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py
deleted file mode 100644
index 57f1a98741..0000000000
--- a/common/djangoapps/terrain/ui_helpers.py
+++ /dev/null
@@ -1,681 +0,0 @@
-# pylint: disable=missing-docstring
-
-import json
-import platform
-import re
-import time
-from textwrap import dedent
-from urllib import quote_plus
-
-# django_url is assigned late in the process of loading lettuce,
-# so we import this as a module, and then read django_url from
-# it to get the correct value
-import lettuce.django
-from lettuce import world
-from selenium.common.exceptions import (
- InvalidElementStateException,
- StaleElementReferenceException,
- TimeoutException,
- WebDriverException
-)
-from selenium.webdriver.common.by import By
-from selenium.webdriver.support import expected_conditions as EC
-from selenium.webdriver.support.ui import WebDriverWait
-
-from openedx.core.lib.tests.tools import assert_true
-
-GLOBAL_WAIT_FOR_TIMEOUT = 60
-
-REQUIREJS_WAIT = {
- # Settings - Schedule & Details
- re.compile(r'^Schedule & Details Settings \|'): [
- "jquery", "js/base", "js/models/course",
- "js/models/settings/course_details", "js/views/settings/main"],
-
- # Settings - Advanced Settings
- re.compile(r'^Advanced Settings \|'): [
- "jquery", "js/base", "js/models/course", "js/models/settings/advanced",
- "js/views/settings/advanced", "codemirror"],
-
- # Content - Outline
- # Note that calling your org, course number, or display name, 'course' will mess this up
- re.compile(r'^Course Outline \|'): [
- "js/base", "js/models/course", "js/models/location", "js/models/section"],
-
- # Dashboard
- re.compile(r'^Studio Home \|'): [
- "gettext", "js/base",
- "jquery.ui", "cms/js/main", "underscore"],
-
- # Pages
- re.compile(r'^Pages \|'): [
- 'js/models/explicit_url', 'js/views/tabs', 'cms/js/main', 'xblock/cms.runtime.v1'
- ],
-}
-
-TRUTHY_WAIT = {
- # Pages
- re.compile(r'^Pages \|'): [
- 'XBlock'
- ],
- # Unit page
- re.compile(r'Unit \|'): [
- "jQuery", "XBlock", "ContainerFactory"
- ],
-
-}
-
-
-@world.absorb
-def wait(seconds):
- time.sleep(float(seconds))
-
-
-@world.absorb
-def wait_for_js_to_load():
- for test, req in REQUIREJS_WAIT.items():
- if test.search(world.browser.title):
- world.wait_for_requirejs(req)
- break
-
- for test, req in TRUTHY_WAIT.items():
- if test.search(world.browser.title):
- for var in req:
- world.wait_for_js_variable_truthy(var)
-
-
-# Selenium's `execute_async_script` function pauses Selenium's execution
-# until the browser calls a specific Javascript callback; in effect,
-# Selenium goes to sleep until the JS callback function wakes it back up again.
-# This callback is passed as the last argument to the script. Any arguments
-# passed to this callback get returned from the `execute_async_script`
-# function, which allows the JS to communicate information back to Python.
-# Ref: https://selenium.googlecode.com/svn/trunk/docs/api/dotnet/html/M_OpenQA_Selenium_IJavaScriptExecutor_ExecuteAsyncScript.htm
-@world.absorb
-def wait_for_js_variable_truthy(variable):
- """
- Using Selenium's `execute_async_script` function, poll the Javascript
- environment until the given variable is defined and truthy. This process
- guards against page reloads, and seamlessly retries on the next page.
- """
- javascript = """
- var callback = arguments[arguments.length - 1];
- var unloadHandler = function() {{
- callback("unload");
- }}
- addEventListener("beforeunload", unloadHandler);
- addEventListener("unload", unloadHandler);
- var intervalID = setInterval(function() {{
- try {{
- if({variable}) {{
- clearInterval(intervalID);
- removeEventListener("beforeunload", unloadHandler);
- removeEventListener("unload", unloadHandler);
- callback(true);
- }}
- }} catch (e) {{}}
- }}, 10);
- """.format(variable=variable)
- for _ in range(5): # 5 attempts max
- try:
- result = world.browser.driver.execute_async_script(dedent(javascript))
- except WebDriverException as wde:
- if "document unloaded while waiting for result" in wde.msg:
- result = "unload"
- else:
- raise
- if result == "unload":
- # we ran this on the wrong page. Wait a bit, and try again, when the
- # browser has loaded the next page.
- world.wait(1)
- continue
- else:
- return result
-
-
-@world.absorb
-def wait_for_xmodule():
- "Wait until the XModule Javascript has loaded on the page."
- world.wait_for_js_variable_truthy("XModule")
- world.wait_for_js_variable_truthy("XBlock")
-
-
-@world.absorb
-def wait_for_mathjax():
- "Wait until MathJax is loaded and set up on the page."
- world.wait_for_js_variable_truthy("MathJax")
-
-
-class RequireJSError(Exception):
- """
- An error related to waiting for require.js. If require.js is unable to load
- a dependency in the `wait_for_requirejs` function, Python will throw
- this exception to make sure that the failure doesn't pass silently.
- """
- pass
-
-
-def load_requrejs_modules(dependencies, callback="callback(true);"):
- javascript = """
- var callback = arguments[arguments.length - 1];
- if(window.require) {{
- requirejs.onError = callback;
- var unloadHandler = function() {{
- callback("unload");
- }}
- addEventListener("beforeunload", unloadHandler);
- addEventListener("unload", unloadHandler);
- require({deps}, function($) {{
- var modules = arguments;
- setTimeout(function() {{
- removeEventListener("beforeunload", unloadHandler);
- removeEventListener("unload", unloadHandler);
- {callback}
- }}, 50);
- }});
- }} else {{
- callback(false);
- }}
- """.format(deps=json.dumps(dependencies), callback=callback)
- for _ in range(5): # 5 attempts max
- try:
- result = world.browser.driver.execute_async_script(dedent(javascript))
- except WebDriverException as wde:
- if "document unloaded while waiting for result" in wde.msg:
- result = "unload"
- else:
- raise
- if result == "unload":
- # we ran this on the wrong page. Wait a bit, and try again, when the
- # browser has loaded the next page.
- world.wait(1)
- continue
- elif result not in (None, True, False):
- # We got a require.js error
- # Sometimes requireJS will throw an error with requireType=require
- # This doesn't seem to cause problems on the page, so we ignore it
- if result['requireType'] == 'require':
- world.wait(1)
- continue
-
- # Otherwise, fail and report the error
- else:
- msg = "Error loading dependencies: type={0} modules={1}".format(
- result['requireType'], result['requireModules'])
- err = RequireJSError(msg)
- err.error = result
- raise err
- else:
- return result
-
-
-def wait_for_xmodules_to_load():
- """
- If requirejs is loaded on the page, this function will pause
- Selenium until require is finished loading all xmodules.
- If requirejs is not loaded on the page, this function will return
- immediately.
- """
- callback = """
- if (modules[0] && modules[0].done) {{
- modules[0].done(function () {{callback(true)}});
- }}
- """
- return load_requrejs_modules(["xmodule"], callback)
-
-
-@world.absorb
-def wait_for_requirejs(dependencies=None):
- """
- If requirejs is loaded on the page, this function will pause
- Selenium until require is finished loading the given dependencies.
- If requirejs is not loaded on the page, this function will return
- immediately.
-
- :param dependencies: a list of strings that identify resources that
- we should wait for requirejs to load. By default, requirejs will only
- wait for jquery.
- """
- if not dependencies:
- dependencies = ["jquery"]
- # stick jquery at the front
- if dependencies[0] != "jquery":
- dependencies.insert(0, "jquery")
-
- result = load_requrejs_modules(dependencies)
- if result and "xmodule" in dependencies:
- result = wait_for_xmodules_to_load()
-
- return result
-
-
-@world.absorb
-def wait_for_ajax_complete():
- """
- Wait until all jQuery AJAX calls have completed. "Complete" means that
- either the server has sent a response (regardless of whether the response
- indicates success or failure), or that the AJAX call timed out waiting for
- a response. For more information about the `jQuery.active` counter that
- keeps track of this information, go here:
- http://stackoverflow.com/questions/3148225/jquery-active-function#3148506
- """
- javascript = """
- var callback = arguments[arguments.length - 1];
- if(!window.jQuery) {callback(false);}
- var intervalID = setInterval(function() {
- if(jQuery.active == 0) {
- clearInterval(intervalID);
- callback(true);
- }
- }, 100);
- """
- # Sometimes the ajax when it returns will make the browser reload
- # the DOM, and throw a WebDriverException with the message:
- # 'javascript error: document unloaded while waiting for result'
- for _ in range(5): # 5 attempts max
- try:
- result = world.browser.driver.execute_async_script(dedent(javascript))
- except WebDriverException as wde:
- if "document unloaded while waiting for result" in wde.msg:
- # Wait a bit, and try again, when the browser has reloaded the page.
- world.wait(1)
- continue
- else:
- raise
- return result
-
-
-@world.absorb
-def visit(url):
- world.browser.visit(lettuce.django.django_url(url))
- wait_for_js_to_load()
-
-
-@world.absorb
-def url_equals(url):
- return world.browser.url == lettuce.django.django_url(url)
-
-
-@world.absorb
-def is_css_present(css_selector, wait_time=30):
- return world.browser.is_element_present_by_css(css_selector, wait_time=wait_time)
-
-
-@world.absorb
-def is_css_not_present(css_selector, wait_time=5):
- world.browser.driver.implicitly_wait(1)
- try:
- return world.browser.is_element_not_present_by_css(css_selector, wait_time=wait_time)
- except:
- raise
- finally:
- world.browser.driver.implicitly_wait(world.IMPLICIT_WAIT)
-
-
-@world.absorb
-def css_has_text(css_selector, text, index=0, strip=False):
- """
- Return a boolean indicating whether the element with `css_selector`
- has `text`.
-
- If `strip` is True, strip whitespace at beginning/end of both
- strings before comparing.
-
- If there are multiple elements matching the css selector,
- use `index` to indicate which one.
- """
- # If we're expecting a non-empty string, give the page
- # a chance to fill in text fields.
- if text:
- wait_for(lambda _: css_text(css_selector, index=index))
-
- actual_text = css_text(css_selector, index=index)
-
- if strip:
- actual_text = actual_text.strip()
- text = text.strip()
-
- return actual_text == text
-
-
-@world.absorb
-def css_contains_text(css_selector, partial_text, index=0):
- """
- Return a boolean indicating whether the element with `css_selector`
- contains `partial_text`.
-
- If there are multiple elements matching the css selector,
- use `index` to indicate which one.
- """
- # If we're expecting a non-empty string, give the page
- # a chance to fill in text fields.
- if partial_text:
- wait_for(lambda _: css_html(css_selector, index=index), timeout=8)
-
- actual_text = css_html(css_selector, index=index)
-
- return partial_text in actual_text
-
-
-@world.absorb
-def css_has_value(css_selector, value, index=0):
- """
- Return a boolean indicating whether the element with
- `css_selector` has the specified `value`.
-
- If there are multiple elements matching the css selector,
- use `index` to indicate which one.
- """
- # If we're expecting a non-empty string, give the page
- # a chance to fill in values
- if value:
- wait_for(lambda _: css_value(css_selector, index=index))
-
- return css_value(css_selector, index=index) == value
-
-
-@world.absorb
-def wait_for(func, timeout=5, timeout_msg=None):
- """
- Calls the method provided with the driver as an argument until the
- return value is not False.
- Throws an error if the WebDriverWait timeout clock expires.
- Otherwise this method will return None.
- """
- msg = timeout_msg or "Timed out after {} seconds.".format(timeout)
- try:
- WebDriverWait(
- driver=world.browser.driver,
- timeout=timeout,
- ignored_exceptions=(StaleElementReferenceException)
- ).until(func)
- except TimeoutException:
- raise TimeoutException(msg)
-
-
-@world.absorb
-def wait_for_present(css_selector, timeout=GLOBAL_WAIT_FOR_TIMEOUT):
- """
- Wait for the element to be present in the DOM.
- """
- wait_for(
- func=lambda _: EC.presence_of_element_located((By.CSS_SELECTOR, css_selector,)),
- timeout=timeout,
- timeout_msg="Timed out waiting for {} to be present.".format(css_selector)
- )
-
-
-@world.absorb
-def wait_for_visible(css_selector, index=0, timeout=GLOBAL_WAIT_FOR_TIMEOUT):
- """
- Wait for the element to be visible in the DOM.
- """
- wait_for(
- func=lambda _: css_visible(css_selector, index),
- timeout=timeout,
- timeout_msg="Timed out waiting for {} to be visible.".format(css_selector)
- )
-
-
-@world.absorb
-def wait_for_invisible(css_selector, timeout=GLOBAL_WAIT_FOR_TIMEOUT):
- """
- Wait for the element to be either invisible or not present on the DOM.
- """
- wait_for(
- func=lambda _: EC.invisibility_of_element_located((By.CSS_SELECTOR, css_selector,)),
- timeout=timeout,
- timeout_msg="Timed out waiting for {} to be invisible.".format(css_selector)
- )
-
-
-@world.absorb
-def wait_for_clickable(css_selector, timeout=GLOBAL_WAIT_FOR_TIMEOUT):
- """
- Wait for the element to be present and clickable.
- """
- wait_for(
- func=lambda _: EC.element_to_be_clickable((By.CSS_SELECTOR, css_selector,)),
- timeout=timeout,
- timeout_msg="Timed out waiting for {} to be clickable.".format(css_selector)
- )
-
-
-@world.absorb
-def css_find(css, wait_time=GLOBAL_WAIT_FOR_TIMEOUT):
- """
- Wait for the element(s) as defined by css locator
- to be present.
-
- This method will return a WebDriverElement.
- """
- wait_for_present(css_selector=css, timeout=wait_time)
- return world.browser.find_by_css(css)
-
-
-@world.absorb
-def css_click(css_selector, index=0, wait_time=GLOBAL_WAIT_FOR_TIMEOUT, dismiss_alert=False):
- """
- Perform a click on a CSS selector, first waiting for the element
- to be present and clickable.
-
- This method will return True if the click worked.
-
- If `dismiss_alert` is true, dismiss any alerts that appear.
- """
- wait_for_clickable(css_selector, timeout=wait_time)
- wait_for_visible(css_selector, index=index, timeout=wait_time)
- assert_true(
- css_visible(css_selector, index=index),
- msg="Element {}[{}] is present but not visible".format(css_selector, index)
- )
-
- retry_on_exception(lambda: css_find(css_selector)[index].click())
-
- # Dismiss any alerts that occur.
- # We need to do this before calling `wait_for_js_to_load()`
- # to avoid getting an unexpected alert exception
- if dismiss_alert:
- world.browser.get_alert().accept()
-
- wait_for_js_to_load()
- return True
-
-
-@world.absorb
-def css_check(css_selector, wait_time=GLOBAL_WAIT_FOR_TIMEOUT):
- """
- Checks a check box based on a CSS selector, first waiting for the element
- to be present and clickable. This is just a wrapper for calling "click"
- because that's how selenium interacts with check boxes and radio buttons.
-
- Then for synchronization purposes, wait for the element to be checked.
- This method will return True if the check worked.
- """
- css_click(css_selector=css_selector, wait_time=wait_time)
- wait_for(lambda _: css_find(css_selector).selected)
- return True
-
-
-@world.absorb
-def select_option(name, value, wait_time=GLOBAL_WAIT_FOR_TIMEOUT):
- '''
- A method to select an option
- Then for synchronization purposes, wait for the option to be selected.
- This method will return True if the selection worked.
- '''
- select_css = "select[name='{}']".format(name)
- option_css = "option[value='{}']".format(value)
-
- css_selector = "{} {}".format(select_css, option_css)
- css_click(css_selector=css_selector, wait_time=wait_time)
- wait_for(lambda _: css_has_value(select_css, value))
- return True
-
-
-@world.absorb
-def id_click(elem_id):
- """
- Perform a click on an element as specified by its id
- """
- css_click('#{}'.format(elem_id))
-
-
-@world.absorb
-def css_fill(css_selector, text, index=0):
- """
- Set the value of the element to the specified text.
- Note that this will replace the current value completely.
- Then for synchronization purposes, wait for the value on the page.
- """
- wait_for_visible(css_selector, index=index)
- retry_on_exception(lambda: css_find(css_selector)[index].fill(text))
- wait_for(lambda _: css_has_value(css_selector, text, index=index))
- return True
-
-
-@world.absorb
-def click_link(partial_text, index=0):
- retry_on_exception(lambda: world.browser.find_link_by_partial_text(partial_text)[index].click())
- wait_for_js_to_load()
-
-
-@world.absorb
-def click_button(data_attr, index=0):
- xpath = '//button[text()="{button_text}"]'.format(
- button_text=data_attr
- )
- world.browser.find_by_xpath(xpath)[index].click()
-
-
-@world.absorb
-def click_link_by_text(text, index=0):
- retry_on_exception(lambda: world.browser.find_link_by_text(text)[index].click())
-
-
-@world.absorb
-def css_text(css_selector, index=0, timeout=GLOBAL_WAIT_FOR_TIMEOUT):
- # Wait for the css selector to appear
- if is_css_present(css_selector):
- return retry_on_exception(lambda: css_find(css_selector, wait_time=timeout)[index].text)
- else:
- return ""
-
-
-@world.absorb
-def css_value(css_selector, index=0):
- # Wait for the css selector to appear
- if is_css_present(css_selector):
- return retry_on_exception(lambda: css_find(css_selector)[index].value)
- else:
- return ""
-
-
-@world.absorb
-def css_html(css_selector, index=0):
- """
- Returns the HTML of a css_selector
- """
- assert is_css_present(css_selector)
- return retry_on_exception(lambda: css_find(css_selector)[index].html)
-
-
-@world.absorb
-def css_has_class(css_selector, class_name, index=0):
- return retry_on_exception(lambda: css_find(css_selector)[index].has_class(class_name))
-
-
-@world.absorb
-def css_visible(css_selector, index=0):
- assert is_css_present(css_selector)
- return retry_on_exception(lambda: css_find(css_selector)[index].visible)
-
-
-@world.absorb
-def dialogs_closed():
- def are_dialogs_closed(_driver):
- '''
- Return True when no modal dialogs are visible
- '''
- return not css_visible('.modal')
- wait_for(are_dialogs_closed)
- return not css_visible('.modal')
-
-
-@world.absorb
-def save_the_html(path='/tmp'):
- url = world.browser.url
- html = world.browser.html.encode('ascii', 'ignore')
- filename = "{path}/{name}.html".format(path=path, name=quote_plus(url))
- with open(filename, "w") as f:
- f.write(html)
-
-
-@world.absorb
-def click_course_content():
- world.wait_for_js_to_load()
- course_content_css = 'li.nav-course-courseware'
- css_click(course_content_css)
-
-
-@world.absorb
-def click_course_settings():
- world.wait_for_js_to_load()
- course_settings_css = 'li.nav-course-settings'
- css_click(course_settings_css)
-
-
-@world.absorb
-def click_tools():
- world.wait_for_js_to_load()
- tools_css = 'li.nav-course-tools'
- css_click(tools_css)
-
-
-@world.absorb
-def is_mac():
- return platform.mac_ver()[0] != ''
-
-
-@world.absorb
-def is_firefox():
- return world.browser.driver_name == 'Firefox'
-
-
-@world.absorb
-def trigger_event(css_selector, event='change', index=0):
- world.browser.execute_script("$('{}:eq({})').trigger('{}')".format(css_selector, index, event))
-
-
-@world.absorb
-def retry_on_exception(func, max_attempts=5, ignored_exceptions=(StaleElementReferenceException, InvalidElementStateException)):
- """
- Retry the interaction, ignoring the passed exceptions.
- By default ignore StaleElementReferenceException, which happens often in our application
- when the DOM is being manipulated by client side JS.
- Note that ignored_exceptions is passed directly to the except block, and as such can be
- either a single exception or multiple exceptions as a parenthesized tuple.
- """
- attempt = 0
- while attempt < max_attempts:
- try:
- return func()
- except ignored_exceptions:
- world.wait(1)
- attempt += 1
-
- assert_true(attempt < max_attempts, 'Ran out of attempts to execute {}'.format(func))
-
-
-@world.absorb
-def disable_jquery_animations():
- """
- Disable JQuery animations on the page. Any state changes
- will occur immediately to the final state.
- """
-
- # Ensure that jquery is loaded
- world.wait_for_js_to_load()
-
- # Disable jQuery animations
- world.browser.execute_script("jQuery.fx.off = true;")
diff --git a/common/test/acceptance/tests/lms/test_lms_problems.py b/common/test/acceptance/tests/lms/test_lms_problems.py
index 14d9af3563..af0fd9bdf5 100644
--- a/common/test/acceptance/tests/lms/test_lms_problems.py
+++ b/common/test/acceptance/tests/lms/test_lms_problems.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
"""
Bok choy acceptance tests for problems in the LMS
-
-See also old lettuce tests in lms/djangoapps/courseware/features/problems.feature
"""
from textwrap import dedent
import time
diff --git a/common/test/acceptance/tests/lms/test_problem_types.py b/common/test/acceptance/tests/lms/test_problem_types.py
index bcac5933ed..7bc35f8e3e 100644
--- a/common/test/acceptance/tests/lms/test_problem_types.py
+++ b/common/test/acceptance/tests/lms/test_problem_types.py
@@ -1,7 +1,5 @@
"""
Bok choy acceptance and a11y tests for problem types in the LMS
-
-See also lettuce tests in lms/djangoapps/courseware/features/problems.feature
"""
import random
import textwrap
diff --git a/common/test/db_cache/lettuce.db b/common/test/db_cache/lettuce.db
deleted file mode 100644
index d69768646b..0000000000
Binary files a/common/test/db_cache/lettuce.db and /dev/null differ
diff --git a/common/test/db_cache/lettuce_student_module_history.db b/common/test/db_cache/lettuce_student_module_history.db
deleted file mode 100644
index 20b9ae9bc6..0000000000
Binary files a/common/test/db_cache/lettuce_student_module_history.db and /dev/null differ
diff --git a/docs/testing.rst b/docs/testing.rst
index 6942682513..8848e7e8e6 100644
--- a/docs/testing.rst
+++ b/docs/testing.rst
@@ -68,13 +68,7 @@ UI Acceptance Tests
- We use `Bok Choy`_ to write end-user acceptance tests directly in Python,
using the framework to maximize reliability and maintainability.
-- We used to use `lettuce`_ to write BDD-style tests but it's now deprecated
- in favor of Bok Choy for new tests. Most of these tests simulate user
- interactions through the browser using `splinter`_.
-
.. _Bok Choy: https://bok-choy.readthedocs.org/en/latest/tutorial.html
-.. _lettuce: http://lettuce.it/
-.. _splinter: http://splinter.cobrateam.info/
Internationalization
@@ -101,8 +95,6 @@ Test Locations
- Set up and helper methods, and stubs for external services:
``common/djangoapps/terrain``
- - Lettuce Tests: located in ``features`` subpackage within a Django
- app. For example: ``lms/djangoapps/courseware/features``
- Bok Choy Acceptance Tests: located under ``common/test/acceptance/tests``
- Bok Choy Accessibility Tests: located under ``common/test/acceptance/tests`` and tagged with ``@attr("a11y")``
- Bok Choy PageObjects: located under ``common/test/acceptance/pages``
@@ -431,8 +423,7 @@ Object and Promise design patterns.
These prerequisites are all automatically installed and available in
`Devstack`_, the supported development enviornment for the Open edX platform.
-* Chromedriver and Chrome (see `Running Lettuce Acceptance Tests`_ below for
- the latest tested versions)
+* Chromedriver and Chrome
* Mongo
@@ -591,65 +582,6 @@ You must run BOTH `--testsonly` and `--fasttest`.
Control-C. *Warning*: Only hit Control-C one time so the pytest framework can
properly clean up.
-Running Lettuce Acceptance Tests
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Although it's deprecated now `lettuce`_ acceptance tests still exists in the
-code base. Most of our tests use `Splinter`_ to simulate UI browser
-interactions. Splinter, in turn, uses `Selenium`_ to control the Chrome
-browser.
-
-**Prerequisite**: You must have `ChromeDriver`_ installed to run the tests in
-Chrome. The tests are confirmed to run with Chrome (not Chromium) version
-34.0.1847.116 with ChromeDriver version 2.6.232917.
-
-.. _ChromeDriver: https://code.google.com/p/selenium/wiki/ChromeDriver
-
-To run all the acceptance tests, run this command::
-
- paver test_acceptance
-
-To run only for lms or cms, run one of these commands::
-
- paver test_acceptance -s lms
- paver test_acceptance -s cms
-
-For example, this command tests only a specific feature::
-
- paver test_acceptance -s lms --extra_args="lms/djangoapps/courseware/features/problems.feature"
-
-A command like this tests only a specific scenario::
-
- paver test_acceptance -s lms --extra_args="lms/djangoapps/courseware/features/problems.feature -s 3"
-
-To start the debugger on failure, pass the ``--pdb`` option to the paver command like this::
-
- paver test_acceptance -s lms --pdb --extra_args="lms/djangoapps/courseware/features/problems.feature"
-
-To run tests faster by not collecting static files or compiling sass, you can use
-``paver test_acceptance -s lms --fasttest`` and
-``paver test_acceptance -s cms --fasttest``.
-
-By default, all acceptance tests are run with the 'draft' ModuleStore.
-To override the modulestore that is used, use the default\_store option.
-Currently, the possible stores for acceptance tests are: 'split'
-(xmodule.modulestore.split\_mongo.split\_draft.DraftVersioningModuleStore)
-and 'draft' (xmodule.modulestore.mongo.DraftMongoModuleStore). For
-example: paver test\_acceptance --default\_store='draft' Note, however,
-all acceptance tests currently do not pass with 'split'.
-
-Acceptance tests will run on a randomized port and can be run in the
-background of paver cms and lms or unit tests. To specify the port,
-change the LETTUCE\_SERVER\_PORT constant in cms/envs/acceptance.py and
-lms/envs/acceptance.py as well as the port listed in
-cms/djangoapps/contentstore/feature/upload.py
-
-During acceptance test execution, Django log files are written to
-``test_root/log/lms_acceptance.log`` and
-``test_root/log/cms_acceptance.log``.
-
-**Note**: The acceptance tests can *not* currently run in parallel.
-
Running Tests on Paver Scripts
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -685,50 +617,6 @@ can find those in the following locations::
Do not commit the ``.po``, ``.mo``, ``.js`` files that are generated
in the above locations during the dummy translation process!
-
-Debugging Acceptance Tests on Vagrant
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-If you are using a local Vagrant dev environment to run acceptance
-tests, then you will only get console text output. To actually see what
-is happening, you can turn on automatic screenshots. For each step two
-screenshots will be taken - before, and after. To do this, simply add
-the step::
-
- Given I enable capturing of screenshots before and after each step
-
-to your scenario. This step can be added anywhere, and will enable
-automatic screenshots for all following steps for that scenario only.
-You can also use the step::
-
- Given I disable capturing of screenshots before and after each step
-
-to turn off auto screenshots for all steps following it.
-
-Screenshots will be placed in the folder
-``{TEST_ROOT}/log/auto_screenshots``. Each time you launch acceptance
-tests, this folder will be cleaned. Each screenshot will be named
-according to the template string
-``{scenario_number}__{step_number}__{step_function_name}__{"1_before"|"2_after"}``.
-
-If you don't want to have screenshots be captured for all steps, but
-rather want fine grained control, you can use this decorator before any Python function in ``feature_name.py`` file::
-
- @capture_screenshot_before_after
-
-The decorator will capture two screenshots: one before the decorated function runs,
-and one after. Also, this function is available, and can be inserted at any point in code to capture a
-screenshot specifically in that place::
-
- from lettuce import world; world.capture_screenshot("image_name")
-
-In both cases the captured screenshots will go to the same folder as when using the step method: ``{TEST_ROOT}/log/auto_screenshot``.
-
-A totally different approach to visually seeing acceptance tests run in
-Vagrant is to redirect Vagrant X11 session to your local machine. Please
-see https://github.com/edx/edx-platform/wiki/Test-engineering-FAQ for
-instruction on how to achieve this.
-
Viewing Test Coverage
---------------------
diff --git a/lms/djangoapps/courseware/features/__init__.py b/lms/djangoapps/courseware/features/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py
deleted file mode 100644
index f07fa08d93..0000000000
--- a/lms/djangoapps/courseware/features/common.py
+++ /dev/null
@@ -1,240 +0,0 @@
-# pylint: disable=missing-docstring
-# pylint: disable=no-member
-# pylint: disable=redefined-outer-name
-
-from __future__ import absolute_import
-
-import time
-from logging import getLogger
-
-from django.contrib.auth.models import User
-from django.urls import reverse
-from lettuce import step, world
-from lettuce.django import django_url
-
-from courseware.courses import get_course_by_id
-from student.models import CourseEnrollment
-from xmodule import seq_module, vertical_block
-from xmodule.course_module import CourseDescriptor
-from xmodule.modulestore.django import modulestore
-
-logger = getLogger(__name__)
-
-
-@step('I (.*) capturing of screenshots before and after each step$')
-def configure_screenshots_for_all_steps(_step, action):
- """
- A step to be used in *.feature files. Enables/disables
- automatic saving of screenshots before and after each step in a
- scenario.
- """
- action = action.strip()
- if action == 'enable':
- world.auto_capture_screenshots = True
- elif action == 'disable':
- world.auto_capture_screenshots = False
- else:
- raise ValueError('Parameter `action` should be one of "enable" or "disable".')
-
-
-@world.absorb
-def capture_screenshot_before_after(func):
- """
- A decorator that will take a screenshot before and after the applied
- function is run. Use this if you do not want to capture screenshots
- for each step in a scenario, but rather want to debug a single function.
- """
- def inner(*args, **kwargs):
- prefix = round(time.time() * 1000)
-
- world.capture_screenshot("{}_{}_{}".format(
- prefix, func.func_name, 'before'
- ))
- ret_val = func(*args, **kwargs)
- world.capture_screenshot("{}_{}_{}".format(
- prefix, func.func_name, 'after'
- ))
- return ret_val
- return inner
-
-
-@step(u'The course "([^"]*)" exists$')
-def create_course(_step, course):
-
- # First clear the modulestore so we don't try to recreate
- # the same course twice
- # This also ensures that the necessary templates are loaded
- world.clear_courses()
-
- # Create the course
- # We always use the same org and display name,
- # but vary the course identifier (e.g. 600x or 191x)
- world.scenario_dict['COURSE'] = world.CourseFactory.create(
- org='edx',
- number=course,
- display_name='Test Course'
- )
-
- # Add a chapter to the course to contain problems
- world.scenario_dict['CHAPTER'] = world.ItemFactory.create(
- parent_location=world.scenario_dict['COURSE'].location,
- category='chapter',
- display_name='Test Chapter',
- publish_item=True, # Not needed for direct-only but I'd rather the test didn't know that
- )
-
- world.scenario_dict['SECTION'] = world.ItemFactory.create(
- parent_location=world.scenario_dict['CHAPTER'].location,
- category='sequential',
- display_name='Test Section',
- publish_item=True,
- )
-
-
-@step(u'I am registered for the course "([^"]*)"$')
-def i_am_registered_for_the_course(step, course):
- # Create the course
- create_course(step, course)
-
- # Create the user
- world.create_user('robot', 'test')
- user = User.objects.get(username='robot')
-
- # If the user is not already enrolled, enroll the user.
- # TODO: change to factory
- CourseEnrollment.enroll(user, course_id(course))
-
- world.log_in(username='robot', password='test')
-
-
-@step(u'The course "([^"]*)" has extra tab "([^"]*)"$')
-def add_tab_to_course(_step, course, extra_tab_name):
- world.ItemFactory.create(
- parent_location=course_location(course),
- category="static_tab",
- display_name=str(extra_tab_name))
-
-
-@step(u'I am in a course$')
-def go_into_course(step):
- step.given('I am registered for the course "6.002x"')
- step.given('And I am logged in')
- step.given('And I click on View Courseware')
-
-
-# Do we really use these 3 w/ a different course than is in the scenario_dict? if so, why? If not,
-# then get rid of the override arg
-def course_id(course_num):
- return world.scenario_dict['COURSE'].id.replace(course=course_num)
-
-
-def course_location(course_num):
- return world.scenario_dict['COURSE'].location.replace(course=course_num)
-
-
-def section_location(course_num):
- return world.scenario_dict['SECTION'].location.replace(course=course_num)
-
-
-def visit_scenario_item(item_key):
- """
- Go to the courseware page containing the item stored in `world.scenario_dict`
- under the key `item_key`
- """
-
- url = django_url(reverse(
- 'jump_to',
- kwargs={
- 'course_id': unicode(world.scenario_dict['COURSE'].id),
- 'location': unicode(world.scenario_dict[item_key].location),
- }
- ))
-
- world.browser.visit(url)
-
-
-def get_courses():
- '''
- Returns dict of lists of courses available, keyed by course.org (ie university).
- Courses are sorted by course.number.
- '''
- courses = [c for c in modulestore().get_courses()
- if isinstance(c, CourseDescriptor)] # skip error descriptors
- courses = sorted(courses, key=lambda course: course.location.course)
- return courses
-
-
-def get_courseware_with_tabs(course_id):
- """
- Given a course_id (string), return a courseware array of dictionaries for the
- top three levels of navigation. Same as get_courseware() except include
- the tabs on the right hand main navigation page.
-
- This hides the appropriate courseware as defined by the hide_from_toc field:
- chapter.hide_from_toc
-
- Example:
-
- [{
- 'chapter_name': 'Overview',
- 'sections': [{
- 'clickable_tab_count': 0,
- 'section_name': 'Welcome',
- 'tab_classes': []
- }, {
- 'clickable_tab_count': 1,
- 'section_name': 'System Usage Sequence',
- 'tab_classes': ['VerticalBlock']
- }, {
- 'clickable_tab_count': 0,
- 'section_name': 'Lab0: Using the tools',
- 'tab_classes': ['HtmlDescriptor', 'HtmlDescriptor', 'CapaDescriptor']
- }, {
- 'clickable_tab_count': 0,
- 'section_name': 'Circuit Sandbox',
- 'tab_classes': []
- }]
- }, {
- 'chapter_name': 'Week 1',
- 'sections': [{
- 'clickable_tab_count': 4,
- 'section_name': 'Administrivia and Circuit Elements',
- 'tab_classes': ['VerticalBlock', 'VerticalBlock', 'VerticalBlock', 'VerticalBlock']
- }, {
- 'clickable_tab_count': 0,
- 'section_name': 'Basic Circuit Analysis',
- 'tab_classes': ['CapaDescriptor', 'CapaDescriptor', 'CapaDescriptor']
- }, {
- 'clickable_tab_count': 0,
- 'section_name': 'Resistor Divider',
- 'tab_classes': []
- }, {
- 'clickable_tab_count': 0,
- 'section_name': 'Week 1 Tutorials',
- 'tab_classes': []
- }]
- }, {
- 'chapter_name': 'Midterm Exam',
- 'sections': [{
- 'clickable_tab_count': 2,
- 'section_name': 'Midterm Exam',
- 'tab_classes': ['VerticalBlock', 'VerticalBlock']
- }]
- }]
- """
-
- course = get_course_by_id(course_id)
- chapters = [chapter for chapter in course.get_children() if not chapter.hide_from_toc]
- courseware = [{
- 'chapter_name': c.display_name_with_default_escaped,
- 'sections': [{
- 'section_name': s.display_name_with_default_escaped,
- 'clickable_tab_count': len(s.get_children()) if isinstance(s, seq_module.SequenceDescriptor) else 0,
- 'tabs': [{
- 'children_count': len(t.get_children()) if isinstance(t, vertical_block.VerticalBlock) else 0,
- 'class': t.__class__.__name__} for t in s.get_children()
- ]
- } for s in c.get_children() if not s.hide_from_toc]
- } for c in chapters]
-
- return courseware
diff --git a/lms/djangoapps/courseware/features/courseware.py b/lms/djangoapps/courseware/features/courseware.py
deleted file mode 100644
index db19fd1970..0000000000
--- a/lms/djangoapps/courseware/features/courseware.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# pylint: disable=missing-docstring
-# pylint: disable=redefined-outer-name
-
-from lettuce import step, world
-from lettuce.django import django_url
-
-
-@step('I visit the courseware URL$')
-def i_visit_the_course_info_url(step):
- url = django_url('/courses/MITx/6.002x/2012_Fall/courseware')
- world.browser.visit(url)
diff --git a/lms/djangoapps/courseware/features/courseware_common.py b/lms/djangoapps/courseware/features/courseware_common.py
deleted file mode 100644
index ba2f2193a7..0000000000
--- a/lms/djangoapps/courseware/features/courseware_common.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# pylint: disable=missing-docstring
-# pylint: disable=redefined-outer-name
-# pylint: disable=unused-argument
-
-from lettuce import step, world
-
-
-@step('I click on View Courseware')
-def i_click_on_view_courseware(step):
- world.css_click('a.enter-course')
-
-
-@step('I click on the "([^"]*)" tab$')
-def i_click_on_the_tab(step, tab_text):
- world.click_link(tab_text)
-
-
-@step('I click the "([^"]*)" button$')
-def i_click_on_the_button(step, data_attr):
- world.click_button(data_attr)
-
-
-@step('I click on the "([^"]*)" link$')
-def i_click_on_the_link(step, link_text):
- world.click_link(link_text)
-
-
-@step('I visit the courseware URL$')
-def i_visit_the_course_info_url(step):
- world.visit('/courses/MITx/6.002x/2012_Fall/courseware')
-
-
-@step(u'I am on the dashboard page$')
-def i_am_on_the_dashboard_page(step):
- assert world.is_css_present('section.courses')
- assert world.url_equals('/dashboard')
-
-
-@step('the "([^"]*)" tab is active$')
-def the_tab_is_active(step, tab_text):
- assert world.css_text('.course-tabs a.active') == tab_text
-
-
-@step('the login dialog is visible$')
-def login_dialog_visible(step):
- assert world.css_visible('form#login_form.login_form')
diff --git a/lms/djangoapps/courseware/features/events.py b/lms/djangoapps/courseware/features/events.py
deleted file mode 100644
index 3133186fce..0000000000
--- a/lms/djangoapps/courseware/features/events.py
+++ /dev/null
@@ -1,82 +0,0 @@
-# pylint: disable=missing-docstring
-
-from django.conf import settings
-from lettuce import before, step, world
-from pymongo import MongoClient
-
-from openedx.core.lib.tests.tools import assert_equals, assert_in # pylint: disable=no-name-in-module
-
-REQUIRED_EVENT_FIELDS = [
- 'agent',
- 'event',
- 'event_source',
- 'event_type',
- 'host',
- 'ip',
- 'page',
- 'time',
- 'username'
-]
-
-
-@before.all # pylint: disable=no-member
-def connect_to_mongodb():
- world.mongo_client = MongoClient(host=settings.MONGO_HOST, port=settings.MONGO_PORT_NUM)
- world.event_collection = world.mongo_client['track']['events']
-
-
-@before.each_scenario # pylint: disable=no-member
-def reset_captured_events(_scenario):
- world.event_collection.drop()
-
-
-@before.outline # pylint: disable=no-member
-def reset_between_outline_scenarios(_scenario, _order, _outline, _reasons_to_fail):
- world.event_collection.drop()
-
-
-@step(r'[aA]n? course url "(.*)" event is emitted$')
-def course_url_event_is_emitted(_step, url_regex):
- event_type = url_regex.format(world.scenario_dict['COURSE'].id) # pylint: disable=no-member
- n_events_are_emitted(_step, 1, event_type, "server")
-
-
-@step(r'([aA]n?|\d+) "(.*)" (server|browser) events? is emitted$')
-def n_events_are_emitted(_step, count, event_type, event_source):
-
- # Ensure all events are written out to mongo before querying.
- world.mongo_client.fsync()
-
- # Note that splinter makes 2 requests when you call browser.visit('/foo')
- # the first just checks to see if the server responds with a status
- # code of 200, the next actually uses the browser to submit the request.
- # We filter out events associated with the status code checks by ignoring
- # events that come directly from splinter.
- criteria = {
- 'event_type': event_type,
- 'event_source': event_source,
- 'agent': {
- '$ne': 'python/splinter'
- }
- }
-
- cursor = world.event_collection.find(criteria)
-
- try:
- number_events = int(count)
- except ValueError:
- number_events = 1
-
- assert_equals(cursor.count(), number_events)
-
- event = cursor.next()
-
- expected_field_values = {
- "username": world.scenario_dict['USER'].username, # pylint: disable=no-member
- "event_type": event_type,
- }
- for key, value in expected_field_values.iteritems():
- assert_equals(event[key], value)
-
- for field in REQUIRED_EVENT_FIELDS:
- assert_in(field, event)
diff --git a/lms/djangoapps/courseware/features/lti.feature b/lms/djangoapps/courseware/features/lti.feature
deleted file mode 100644
index 39d9b9bd6d..0000000000
--- a/lms/djangoapps/courseware/features/lti.feature
+++ /dev/null
@@ -1,150 +0,0 @@
-@shard_1 @requires_stub_lti
-Feature: LMS.LTI component
- As a student, I want to view LTI component in LMS.
-
- #1
- Scenario: LTI component in LMS with no launch_url is not rendered
- Given the course has correct LTI credentials with registered Instructor
- And the course has an LTI component with no_launch_url fields:
- | open_in_a_new_page |
- | False |
- Then I view the LTI and error is shown
-
- #2
- Scenario: LTI component in LMS with incorrect lti_id is rendered incorrectly
- Given the course has correct LTI credentials with registered Instructor
- And the course has an LTI component with incorrect_lti_id fields:
- | open_in_a_new_page |
- | False |
- Then I view the LTI but incorrect_signature warning is rendered
-
- #3
- Scenario: LTI component in LMS is rendered incorrectly
- Given the course has incorrect LTI credentials
- And the course has an LTI component with correct fields:
- | open_in_a_new_page |
- | False |
- Then I view the LTI but incorrect_signature warning is rendered
-
- #5
- Scenario: LTI component in LMS is correctly rendered in iframe
- Given the course has correct LTI credentials with registered Instructor
- And the course has an LTI component with correct fields:
- | open_in_a_new_page |
- | False |
- Then I view the LTI and it is rendered in iframe
-
- #6
- Scenario: Graded LTI component in LMS is correctly works
- Given the course has correct LTI credentials with registered Instructor
- And the course has an LTI component with correct fields:
- | open_in_a_new_page | weight | graded | has_score |
- | False | 10 | True | True |
- And I submit answer to LTI 1 question
- And I click on the "Progress" tab
- Then I see text "Problem Scores: 5/10"
- And I see graph with total progress "5%"
- Then I click on the "Instructor" tab
- And I click the "Student Admin" button
- And I click on the "View Gradebook" link
- And I see in the gradebook table that "HW" is "50"
- And I see in the gradebook table that "Total" is "5"
-
- #7
- Scenario: Graded LTI component in LMS role's masquerading correctly works
- Given the course has correct LTI credentials with registered Instructor
- And the course has an LTI component with correct fields:
- | open_in_a_new_page | has_score |
- | False | True |
- And I view the LTI and it is rendered in iframe
- And I see in iframe that LTI role is Instructor
- And I switch to student
- And I view the LTI and it is rendered in iframe
- Then I see in iframe that LTI role is Student
-
- #8
- Scenario: Graded LTI component in LMS is correctly works with beta testers
- Given the course has correct LTI credentials with registered BetaTester
- And the course has an LTI component with correct fields:
- | open_in_a_new_page | weight | graded | has_score |
- | False | 10 | True | True |
- And I submit answer to LTI 1 question
- And I click on the "Progress" tab
- Then I see text "Problem Scores: 5/10"
- And I see graph with total progress "5%"
-
- #9
- Scenario: Graded LTI component in LMS is correctly works with LTI2v0 PUT callback
- Given the course has correct LTI credentials with registered Instructor
- And the course has an LTI component with correct fields:
- | open_in_a_new_page | weight | graded | has_score |
- | False | 10 | True | True |
- And I submit answer to LTI 2 question
- And I click on the "Progress" tab
- Then I see text "Problem Scores: 8/10"
- And I see graph with total progress "8%"
- Then I click on the "Instructor" tab
- And I click the "Student Admin" button
- And I click on the "View Gradebook" link
- And I see in the gradebook table that "HW" is "80"
- And I see in the gradebook table that "Total" is "8"
- And I visit the LTI component
- Then I see LTI component progress with text "(8.0 / 10.0 points)"
- Then I see LTI component feedback with text "This is awesome."
-
- #10
- Scenario: Graded LTI component in LMS is correctly works with LTI2v0 PUT delete callback
- Given the course has correct LTI credentials with registered Instructor
- And the course has an LTI component with correct fields:
- | open_in_a_new_page | weight | graded | has_score |
- | False | 10 | True | True |
- And I submit answer to LTI 2 question
- And I visit the LTI component
- Then I see LTI component progress with text "(8.0 / 10.0 points)"
- Then I see LTI component feedback with text "This is awesome."
- And the LTI provider deletes my grade and feedback
- And I visit the LTI component (have to reload)
- Then I see LTI component progress with text "(10.0 points possible)"
- Then in the LTI component I do not see feedback
- And I click on the "Progress" tab
- Then I see text "Problem Scores: 0/10"
- And I see graph with total progress "0%"
- Then I click on the "Instructor" tab
- And I click the "Student Admin" button
- And I click on the "View Gradebook" link
- And I see in the gradebook table that "HW" is "0"
- And I see in the gradebook table that "Total" is "0"
-
- #11
- Scenario: LTI component that set to hide_launch and open_in_a_new_page shows no button
- Given the course has correct LTI credentials with registered Instructor
- And the course has an LTI component with correct fields:
- | open_in_a_new_page | hide_launch |
- | False | True |
- Then in the LTI component I do not see a launch button
- Then I see LTI component module title with text "LTI (External resource)"
-
- #12
- Scenario: LTI component that set to hide_launch and not open_in_a_new_page shows no iframe
- Given the course has correct LTI credentials with registered Instructor
- And the course has an LTI component with correct fields:
- | open_in_a_new_page | hide_launch |
- | True | True |
- Then in the LTI component I do not see an provider iframe
- Then I see LTI component module title with text "LTI (External resource)"
-
- #13
- Scenario: LTI component button text is correctly displayed
- Given the course has correct LTI credentials with registered Instructor
- And the course has an LTI component with correct fields:
- | button_text |
- | Launch Application |
- Then I see LTI component button with text "Launch Application"
-
- #14
- Scenario: LTI component description is correctly displayed
- Given the course has correct LTI credentials with registered Instructor
- And the course has an LTI component with correct fields:
- | description |
- | Application description |
- Then I see LTI component description with text "Application description"
diff --git a/lms/djangoapps/courseware/features/lti.py b/lms/djangoapps/courseware/features/lti.py
deleted file mode 100644
index a0e273802a..0000000000
--- a/lms/djangoapps/courseware/features/lti.py
+++ /dev/null
@@ -1,345 +0,0 @@
-# pylint: disable=missing-docstring
-# pylint: disable=no-member
-import datetime
-import os
-
-import pytz
-from django.conf import settings
-from lettuce import step, world
-from mock import patch
-from pytz import UTC
-from splinter.exceptions import ElementDoesNotExist
-
-from common import visit_scenario_item
-from courseware.access import has_access
-from courseware.tests.factories import BetaTesterFactory, InstructorFactory
-from openedx.core.lib.tests.tools import assert_equal, assert_in, assert_true # pylint: disable=no-name-in-module
-from student.tests.factories import UserFactory
-
-TEST_COURSE_NAME = "test_course_a"
-
-
-@step('I view the LTI and error is shown$')
-def lti_is_not_rendered(_step):
- # error is shown
- assert world.is_css_present('.error_message', wait_time=0)
-
- # iframe is not presented
- assert not world.is_css_present('iframe', wait_time=0)
-
- # link is not presented
- assert not world.is_css_present('.link_lti_new_window', wait_time=0)
-
-
-def check_lti_iframe_content(text):
- # inside iframe test content is presented
- location = world.scenario_dict['LTI'].location.html_id()
- iframe_name = 'ltiFrame-' + location
- with world.browser.get_iframe(iframe_name) as iframe:
- # iframe does not contain functions from terrain/ui_helpers.py
- assert iframe.is_element_present_by_css('.result', wait_time=0)
- assert (text == world.retry_on_exception(
- lambda: iframe.find_by_css('.result')[0].text,
- max_attempts=5
- ))
-
-
-@step('I view the LTI and it is rendered in iframe$')
-def lti_is_rendered_iframe(_step):
- world.wait_for_present('iframe') # pylint: disable=no-member
- assert world.is_css_present('iframe', wait_time=2) # pylint: disable=no-member
- assert not world.is_css_present('.link_lti_new_window', wait_time=0) # pylint: disable=no-member
- assert not world.is_css_present('.error_message', wait_time=0) # pylint: disable=no-member
-
- # iframe is visible
- assert world.css_visible('iframe') # pylint: disable=no-member
- check_lti_iframe_content("This is LTI tool. Success.")
-
-
-@step('I view the LTI but incorrect_signature warning is rendered$')
-def incorrect_lti_is_rendered(_step):
- assert world.is_css_present('iframe', wait_time=2)
- assert not world.is_css_present('.link_lti_new_window', wait_time=0)
- assert not world.is_css_present('.error_message', wait_time=0)
-
- # inside iframe test content is presented
- check_lti_iframe_content("Wrong LTI signature")
-
-
-@step('the course has correct LTI credentials with registered (.*)$')
-def set_correct_lti_passport(_step, user='Instructor'):
- coursenum = TEST_COURSE_NAME
- metadata = {
- 'lti_passports': ["correct_lti_id:test_client_key:test_client_secret"]
- }
-
- i_am_registered_for_the_course(coursenum, metadata, user)
-
-
-@step('the course has incorrect LTI credentials$')
-def set_incorrect_lti_passport(_step):
- coursenum = TEST_COURSE_NAME
- metadata = {
- 'lti_passports': ["test_lti_id:test_client_key:incorrect_lti_secret_key"]
- }
-
- i_am_registered_for_the_course(coursenum, metadata)
-
-
-@step(r'the course has an LTI component with (.*) fields(?:\:)?$') # , new_page is(.*), graded is(.*)
-def add_correct_lti_to_course(_step, fields):
- category = 'lti'
- host = getattr(settings, 'LETTUCE_HOST', '127.0.0.1')
- metadata = {
- 'lti_id': 'correct_lti_id',
- 'launch_url': 'http://{}:{}/correct_lti_endpoint'.format(host, settings.LTI_PORT),
- }
-
- if fields.strip() == 'incorrect_lti_id': # incorrect fields
- metadata.update({
- 'lti_id': 'incorrect_lti_id'
- })
- elif fields.strip() == 'correct': # correct fields
- pass
- elif fields.strip() == 'no_launch_url':
- metadata.update({
- 'launch_url': u''
- })
- else: # incorrect parameter
- assert False
-
- if _step.hashes:
- metadata.update(_step.hashes[0])
-
- world.scenario_dict['LTI'] = world.ItemFactory.create(
- parent_location=world.scenario_dict['SECTION'].location,
- category=category,
- display_name='LTI',
- metadata=metadata,
- )
-
- visit_scenario_item('LTI')
-
-
-def create_course_for_lti(course, metadata):
- # First clear the modulestore so we don't try to recreate
- # the same course twice
- # This also ensures that the necessary templates are loaded
- world.clear_courses()
-
- weight = 0.1
- grading_policy = {
- "GRADER": [
- {
- "type": "Homework",
- "min_count": 1,
- "drop_count": 0,
- "short_label": "HW",
- "weight": weight
- },
- ]
- }
-
- # Create the course
- # We always use the same org and display name,
- # but vary the course identifier (e.g. 600x or 191x)
- world.scenario_dict['COURSE'] = world.CourseFactory.create(
- org='edx',
- number=course,
- display_name='Test Course',
- metadata=metadata,
- grading_policy=grading_policy,
- )
-
- # Add a section to the course to contain problems
- world.scenario_dict['CHAPTER'] = world.ItemFactory.create(
- parent_location=world.scenario_dict['COURSE'].location,
- category='chapter',
- display_name='Test Chapter',
- )
- world.scenario_dict['SECTION'] = world.ItemFactory.create(
- parent_location=world.scenario_dict['CHAPTER'].location,
- category='sequential',
- display_name='Test Section',
- metadata={'graded': True, 'format': 'Homework'})
-
-
-@patch.dict('courseware.access.settings.FEATURES', {'DISABLE_START_DATES': False})
-def i_am_registered_for_the_course(coursenum, metadata, user='Instructor'):
- # Create user
- if user == 'BetaTester':
- # Create the course
- now = datetime.datetime.now(pytz.UTC)
- tomorrow = now + datetime.timedelta(days=5)
- metadata.update({'days_early_for_beta': 5, 'start': tomorrow})
- create_course_for_lti(coursenum, metadata)
- course_descriptor = world.scenario_dict['COURSE']
-
- # create beta tester
- user = BetaTesterFactory(course_key=course_descriptor.id)
- normal_student = UserFactory()
- instructor = InstructorFactory(course_key=course_descriptor.id)
-
- assert not has_access(normal_student, 'load', course_descriptor)
- assert has_access(user, 'load', course_descriptor)
- assert has_access(instructor, 'load', course_descriptor)
- else:
- metadata.update({'start': datetime.datetime(1970, 1, 1, tzinfo=UTC)})
- create_course_for_lti(coursenum, metadata)
- course_descriptor = world.scenario_dict['COURSE']
- user = InstructorFactory(course_key=course_descriptor.id)
-
- # Enroll the user in the course and log them in
- if has_access(user, 'load', course_descriptor):
- world.enroll_user(user, course_descriptor.id)
-
- world.log_in(username=user.username, password='test')
-
-
-def check_lti_popup(parent_window):
- # You should now have 2 browser windows open, the original courseware and the LTI
- windows = world.browser.windows
- assert_equal(len(windows), 2)
-
- # For verification, iterate through the window titles and make sure that
- # both are there.
- tabs = []
- expected_tabs = [
- u'LTI | Test Section | {course} Courseware | {platform}'.format(
- course=TEST_COURSE_NAME,
- platform=settings.PLATFORM_NAME
- ),
- u'TEST TITLE'
- ]
-
- for window in windows:
- world.browser.switch_to_window(window)
- tabs.append(world.browser.title)
- assert_equal(tabs, expected_tabs)
-
- # Now verify the contents of the LTI window (which is the 2nd window/tab)
- # Note: The LTI opens in a new browser window, but Selenium sticks with the
- # current window until you explicitly switch to the context of the new one.
- world.browser.switch_to_window(windows[1])
- url = world.browser.url
- basename = os.path.basename(url)
- pathname = os.path.splitext(basename)[0]
- assert_equal(pathname, u'correct_lti_endpoint')
-
- result = world.css_find('.result').first.text
- assert_equal(result, u'This is LTI tool. Success.')
-
- world.browser.driver.close() # Close the pop-up window
- world.browser.switch_to_window(parent_window) # Switch to the main window again
-
-
-def click_and_check_lti_popup():
- parent_window = world.browser.current_window # Save the parent window
- world.css_find('.link_lti_new_window').first.click()
- check_lti_popup(parent_window)
-
-
-@step('visit the LTI component')
-def visit_lti_component(_step):
- visit_scenario_item('LTI')
-
-
-@step('I see LTI component (.*) with text "([^"]*)"$')
-def see_elem_text(_step, elem, text):
- selector_map = {
- 'progress': '.problem-progress',
- 'feedback': '.problem-feedback',
- 'module title': '.problem-header',
- 'button': '.link_lti_new_window',
- 'description': '.lti-description'
- }
- assert_in(elem, selector_map)
- assert_true(world.css_has_text(selector_map[elem], text))
-
-
-@step('I see text "([^"]*)"$')
-def check_progress(_step, text):
- assert world.browser.is_text_present(text)
-
-
-@step('I see graph with total progress "([^"]*)"$')
-def see_graph(_step, progress):
- assert_equal(progress, world.css_find('#grade-detail-graph .overallGrade').first.text.split('\n')[1])
-
-
-@step('I see in the gradebook table that "([^"]*)" is "([^"]*)"$')
-def see_value_in_the_gradebook(_step, label, text):
- table_selector = '.grade-table'
- index = 0
- table_headers = world.css_find(u'{0} thead th'.format(table_selector))
-
- for i, element in enumerate(table_headers):
- if element.text.strip() == label:
- index = i
- break
-
- assert_true(world.css_has_text(u'{0} tbody td'.format(table_selector), text, index=index))
-
-
-@step('I submit answer to LTI (.*) question$')
-def click_grade(_step, version):
- version_map = {
- '1': {'selector': 'submit-button', 'expected_text': 'LTI consumer (edX) responded with XML content'},
- '2': {'selector': 'submit-lti2-button', 'expected_text': 'LTI consumer (edX) responded with HTTP 200'},
- }
- assert_in(version, version_map)
- location = world.scenario_dict['LTI'].location.html_id()
- iframe_name = 'ltiFrame-' + location
- with world.browser.get_iframe(iframe_name) as iframe:
- css_ele = version_map[version]['selector']
- css_loc = '#' + css_ele
- world.wait_for_visible(css_loc)
- world.css_click(css_loc)
- assert iframe.is_text_present(version_map[version]['expected_text'])
-
-
-@step('LTI provider deletes my grade and feedback$')
-def click_delete_button(_step):
- with world.browser.get_iframe(get_lti_frame_name()) as iframe:
- iframe.find_by_name('submit-lti2-delete-button').first.click()
-
-
-def get_lti_frame_name():
- location = world.scenario_dict['LTI'].location.html_id()
- return 'ltiFrame-' + location
-
-
-@step('I see in iframe that LTI role is (.*)$')
-def check_role(_step, role):
- world.wait_for_present('iframe')
- location = world.scenario_dict['LTI'].location.html_id()
- iframe_name = 'ltiFrame-' + location
- with world.browser.get_iframe(iframe_name) as iframe:
- expected_role = 'Role: ' + role
- role = world.retry_on_exception(
- lambda: iframe.find_by_tag('h5').first.value,
- max_attempts=5,
- ignored_exceptions=ElementDoesNotExist
- )
- assert_equal(expected_role, role)
-
-
-@step('I switch to (.*)$')
-def switch_view(_step, view):
- staff_status = world.css_find('#action-preview-select').first.value
- if staff_status != view:
- world.browser.select("select", view)
- world.wait_for_ajax_complete()
- assert_equal(world.css_find('#action-preview-select').first.value, view)
-
-
-@step("in the LTI component I do not see (.*)$")
-def check_lti_component_no_elem(_step, text):
- selector_map = {
- 'a launch button': '.link_lti_new_window',
- 'an provider iframe': '.ltiLaunchFrame',
- 'feedback': '.problem-feedback',
- 'progress': '.problem-progress',
- }
- assert_in(text, selector_map)
- assert_true(world.is_css_not_present(selector_map[text]))
diff --git a/lms/djangoapps/courseware/features/problems_setup.py b/lms/djangoapps/courseware/features/problems_setup.py
deleted file mode 100644
index 1713ef0fb1..0000000000
--- a/lms/djangoapps/courseware/features/problems_setup.py
+++ /dev/null
@@ -1,460 +0,0 @@
-# pylint: disable=missing-docstring
-
-# EVERY PROBLEM TYPE MUST HAVE THE FOLLOWING:
-# -Section in Dictionary containing:
-# -factory
-# -kwargs
-# -(optional metadata)
-# -Correct, Incorrect and Unanswered CSS selectors
-# -A way to answer the problem correctly and incorrectly
-# -A way to check the problem was answered correctly, incorrectly and blank
-
-import random
-import textwrap
-
-from lettuce import world
-
-from capa.tests.response_xml_factory import (
- ChoiceResponseXMLFactory,
- ChoiceTextResponseXMLFactory,
- CodeResponseXMLFactory,
- CustomResponseXMLFactory,
- FormulaResponseXMLFactory,
- ImageResponseXMLFactory,
- MultipleChoiceResponseXMLFactory,
- NumericalResponseXMLFactory,
- OptionResponseXMLFactory,
- StringResponseXMLFactory
-)
-from common import section_location
-
-# Factories from capa.tests.response_xml_factory that we will use
-# to generate the problem XML, with the keyword args used to configure
-# the output.
-# 'correct', 'incorrect', and 'unanswered' keys are lists of CSS selectors
-# the presence of any in the list is sufficient
-PROBLEM_DICT = {
- 'drop down': {
- 'factory': OptionResponseXMLFactory(),
- 'kwargs': {
- 'question_text': 'The correct answer is Option 2',
- 'options': ['Option 1', 'Option 2', 'Option 3', 'Option 4'],
- 'correct_option': 'Option 2'},
- 'correct': ['span.correct'],
- 'incorrect': ['span.incorrect'],
- 'unanswered': ['span.unanswered']},
-
- 'multiple choice': {
- 'factory': MultipleChoiceResponseXMLFactory(),
- 'kwargs': {
- 'question_text': 'The correct answer is Choice 3',
- 'choices': [False, False, True, False],
- 'choice_names': ['choice_0', 'choice_1', 'choice_2', 'choice_3']},
- 'correct': ['label.choicegroup_correct', 'span.correct'],
- 'incorrect': ['label.choicegroup_incorrect', 'span.incorrect'],
- 'unanswered': ['span.unanswered']},
-
- 'checkbox': {
- 'factory': ChoiceResponseXMLFactory(),
- 'kwargs': {
- 'question_text': 'The correct answer is Choices 1 and 3',
- 'choice_type': 'checkbox',
- 'choices': [True, False, True, False, False],
- 'choice_names': ['Choice 1', 'Choice 2', 'Choice 3', 'Choice 4']},
- 'correct': ['span.correct'],
- 'incorrect': ['span.incorrect'],
- 'unanswered': ['span.unanswered']},
-
- 'radio': {
- 'factory': ChoiceResponseXMLFactory(),
- 'kwargs': {
- 'question_text': 'The correct answer is Choice 3',
- 'choice_type': 'radio',
- 'choices': [False, False, True, False],
- 'choice_names': ['Choice 1', 'Choice 2', 'Choice 3', 'Choice 4']},
- 'correct': ['label.choicegroup_correct', 'span.correct'],
- 'incorrect': ['label.choicegroup_incorrect', 'span.incorrect'],
- 'unanswered': ['span.unanswered']},
-
- 'string': {
- 'factory': StringResponseXMLFactory(),
- 'kwargs': {
- 'question_text': 'The answer is "correct string"',
- 'case_sensitive': False,
- 'answer': 'correct string'},
- 'correct': ['div.correct'],
- 'incorrect': ['div.incorrect'],
- 'unanswered': ['div.unanswered', 'div.unsubmitted']},
-
- 'numerical': {
- 'factory': NumericalResponseXMLFactory(),
- 'kwargs': {
- 'question_text': 'The answer is pi + 1',
- 'answer': '4.14159',
- 'tolerance': '0.00001',
- 'math_display': True},
- 'correct': ['div.correct'],
- 'incorrect': ['div.incorrect'],
- 'unanswered': ['div.unanswered', 'div.unsubmitted']},
-
- 'formula': {
- 'factory': FormulaResponseXMLFactory(),
- 'kwargs': {
- 'question_text': 'The solution is [mathjax]x^2+2x+y[/mathjax]',
- 'sample_dict': {'x': (-100, 100), 'y': (-100, 100)},
- 'num_samples': 10,
- 'tolerance': 0.00001,
- 'math_display': True,
- 'answer': 'x^2+2*x+y'},
- 'correct': ['div.correct'],
- 'incorrect': ['div.incorrect'],
- 'unanswered': ['div.unanswered', 'div.unsubmitted']},
-
- 'script': {
- 'factory': CustomResponseXMLFactory(),
- 'kwargs': {
- 'question_text': 'Enter two integers that sum to 10.',
- 'cfn': 'test_add_to_ten',
- 'expect': '10',
- 'num_inputs': 2,
- 'script': textwrap.dedent("""
- def test_add_to_ten(expect,ans):
- try:
- a1=int(ans[0])
- a2=int(ans[1])
- except ValueError:
- a1=0
- a2=0
- return (a1+a2)==int(expect)
- """)},
- 'correct': ['div.correct'],
- 'incorrect': ['div.incorrect'],
- 'unanswered': ['div.unanswered', 'div.unsubmitted']},
-
- 'code': {
- 'factory': CodeResponseXMLFactory(),
- 'kwargs': {
- 'question_text': 'Submit code to an external grader',
- 'initial_display': 'print "Hello world!"',
- 'grader_payload': '{"grader": "ps1/Spring2013/test_grader.py"}', },
- 'correct': ['span.correct'],
- 'incorrect': ['span.incorrect'],
- 'unanswered': ['span.unanswered']},
-
- 'radio_text': {
- 'factory': ChoiceTextResponseXMLFactory(),
- 'kwargs': {
- 'question_text': 'The correct answer is Choice 0 and input 8',
- 'type': 'radiotextgroup',
- 'choices': [("true", {"answer": "8", "tolerance": "1"}),
- ("false", {"answer": "8", "tolerance": "1"})
- ]
- },
- 'correct': ['section.choicetextgroup_correct'],
- 'incorrect': ['section.choicetextgroup_incorrect', 'span.incorrect'],
- 'unanswered': ['span.unanswered']},
-
- 'checkbox_text': {
- 'factory': ChoiceTextResponseXMLFactory(),
- 'kwargs': {
- 'question_text': 'The correct answer is Choice 0 and input 8',
- 'type': 'checkboxtextgroup',
- 'choices': [("true", {"answer": "8", "tolerance": "1"}),
- ("false", {"answer": "8", "tolerance": "1"})
- ]
- },
- 'correct': ['span.correct'],
- 'incorrect': ['span.incorrect'],
- 'unanswered': ['span.unanswered']},
-
- 'image': {
- 'factory': ImageResponseXMLFactory(),
- 'kwargs': {
- 'src': '/static/images/placeholder-image.png',
- 'rectangle': '(50,50)-(100,100)'
- },
- 'correct': ['span.correct'],
- 'incorrect': ['span.incorrect'],
- 'unanswered': ['span.unanswered']}
-}
-
-
-def answer_problem(course, problem_type, correctness):
- # Make sure that the problem has been completely rendered before
- # starting to input an answer.
- world.wait_for_ajax_complete()
-
- section_loc = section_location(course)
-
- if problem_type == "drop down":
- select_name = "input_{}_2_1".format(
- section_loc.course_key.make_usage_key('problem', 'drop_down').html_id()
- )
- option_text = 'Option 2' if correctness == 'correct' else 'Option 3'
- world.select_option(select_name, option_text)
-
- elif problem_type == "multiple choice":
- if correctness == 'correct':
- world.css_check(inputfield(course, 'multiple choice', choice='choice_2'))
- else:
- world.css_check(inputfield(course, 'multiple choice', choice='choice_1'))
-
- elif problem_type == "checkbox":
- if correctness == 'correct':
- world.css_check(inputfield(course, 'checkbox', choice='choice_0'))
- world.css_check(inputfield(course, 'checkbox', choice='choice_2'))
- else:
- world.css_check(inputfield(course, 'checkbox', choice='choice_3'))
-
- elif problem_type == 'radio':
- if correctness == 'correct':
- world.css_check(inputfield(course, 'radio', choice='choice_2'))
- else:
- world.css_check(inputfield(course, 'radio', choice='choice_1'))
-
- elif problem_type == 'string':
- textvalue = 'correct string' if correctness == 'correct' else 'incorrect'
- world.css_fill(inputfield(course, 'string'), textvalue)
-
- elif problem_type == 'numerical':
- textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2, 2))
- world.css_fill(inputfield(course, 'numerical'), textvalue)
-
- elif problem_type == 'formula':
- textvalue = "x^2+2*x+y" if correctness == 'correct' else 'x^2'
- world.css_fill(inputfield(course, 'formula'), textvalue)
-
- elif problem_type == 'script':
- # Correct answer is any two integers that sum to 10
- first_addend = random.randint(-100, 100)
- second_addend = 10 - first_addend
-
- # If we want an incorrect answer, then change
- # the second addend so they no longer sum to 10
- if correctness == 'incorrect':
- second_addend += random.randint(1, 10)
-
- world.css_fill(inputfield(course, 'script', input_num=1), str(first_addend))
- world.css_fill(inputfield(course, 'script', input_num=2), str(second_addend))
-
- elif problem_type == 'code':
- # The fake xqueue server is configured to respond
- # correct / incorrect no matter what we submit.
- # Furthermore, since the inline code response uses
- # JavaScript to make the code display nicely, it's difficult
- # to programatically input text
- # (there's not