From bfe10720c60c902056825ab193cf4f820fdd81f5 Mon Sep 17 00:00:00 2001 From: Michael Youngstrom Date: Fri, 12 Apr 2019 12:57:59 -0400 Subject: [PATCH] Remove lettuce infrastructure --- .../contentstore/features/__init__.py | 0 .../features/advanced_settings.py | 25 - .../contentstore/features/common.py | 406 ----------- .../contentstore/features/component.py | 181 ----- .../component_settings_editor_helpers.py | 270 ------- .../contentstore/features/course-settings.py | 170 ----- .../contentstore/features/course_import.py | 27 - .../contentstore/features/html-editor.feature | 128 ---- .../contentstore/features/html-editor.py | 306 -------- .../features/problem-editor.feature | 66 -- .../contentstore/features/problem-editor.py | 391 ---------- .../contentstore/features/signup.py | 71 -- cms/envs/acceptance.py | 149 ---- cms/envs/acceptance_docker.py | 61 -- cms/envs/test.py | 1 - common/djangoapps/terrain/__init__.py | 14 - common/djangoapps/terrain/browser.py | 288 -------- common/djangoapps/terrain/course_helpers.py | 77 -- common/djangoapps/terrain/factories.py | 30 - common/djangoapps/terrain/setup_prereqs.py | 162 ----- common/djangoapps/terrain/steps.py | 244 ------- common/djangoapps/terrain/ui_helpers.py | 681 ------------------ .../acceptance/tests/lms/test_lms_problems.py | 2 - .../tests/lms/test_problem_types.py | 2 - common/test/db_cache/lettuce.db | Bin 1842176 -> 0 bytes .../lettuce_student_module_history.db | Bin 41984 -> 0 bytes docs/testing.rst | 114 +-- .../courseware/features/__init__.py | 0 lms/djangoapps/courseware/features/common.py | 240 ------ .../courseware/features/courseware.py | 11 - .../courseware/features/courseware_common.py | 46 -- lms/djangoapps/courseware/features/events.py | 82 --- .../courseware/features/lti.feature | 150 ---- lms/djangoapps/courseware/features/lti.py | 345 --------- .../courseware/features/problems_setup.py | 460 ------------ .../courseware/features/registration.py | 61 -- lms/djangoapps/courseware/module_render.py | 17 +- .../instructor/features/__init__.py | 0 lms/djangoapps/instructor/features/common.py | 136 ---- .../instructor/features/data_download.py | 94 --- lms/envs/acceptance.py | 215 ------ lms/envs/acceptance_docker.py | 82 --- lms/envs/test.py | 3 - openedx/core/lib/tests/tools.py | 44 -- pavelib/__init__.py | 3 +- pavelib/acceptance_test.py | 65 -- pavelib/utils/envs.py | 5 +- pavelib/utils/test/suites/__init__.py | 1 - pavelib/utils/test/suites/acceptance_suite.py | 161 ----- requirements/constraints.txt | 3 - requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 14 +- requirements/edx/testing.in | 4 - requirements/edx/testing.txt | 16 +- scripts/Jenkinsfiles/lettuce | 151 ---- scripts/dependencies/testing.py | 2 - scripts/generic-ci-tests.sh | 12 - 57 files changed, 9 insertions(+), 6282 deletions(-) delete mode 100644 cms/djangoapps/contentstore/features/__init__.py delete mode 100644 cms/djangoapps/contentstore/features/advanced_settings.py delete mode 100644 cms/djangoapps/contentstore/features/common.py delete mode 100644 cms/djangoapps/contentstore/features/component.py delete mode 100644 cms/djangoapps/contentstore/features/component_settings_editor_helpers.py delete mode 100644 cms/djangoapps/contentstore/features/course-settings.py delete mode 100644 cms/djangoapps/contentstore/features/course_import.py delete mode 100644 cms/djangoapps/contentstore/features/html-editor.feature delete mode 100644 cms/djangoapps/contentstore/features/html-editor.py delete mode 100644 cms/djangoapps/contentstore/features/problem-editor.feature delete mode 100644 cms/djangoapps/contentstore/features/problem-editor.py delete mode 100644 cms/djangoapps/contentstore/features/signup.py delete mode 100644 cms/envs/acceptance.py delete mode 100644 cms/envs/acceptance_docker.py delete mode 100644 common/djangoapps/terrain/browser.py delete mode 100644 common/djangoapps/terrain/course_helpers.py delete mode 100644 common/djangoapps/terrain/factories.py delete mode 100644 common/djangoapps/terrain/setup_prereqs.py delete mode 100644 common/djangoapps/terrain/steps.py delete mode 100644 common/djangoapps/terrain/ui_helpers.py delete mode 100644 common/test/db_cache/lettuce.db delete mode 100644 common/test/db_cache/lettuce_student_module_history.db delete mode 100644 lms/djangoapps/courseware/features/__init__.py delete mode 100644 lms/djangoapps/courseware/features/common.py delete mode 100644 lms/djangoapps/courseware/features/courseware.py delete mode 100644 lms/djangoapps/courseware/features/courseware_common.py delete mode 100644 lms/djangoapps/courseware/features/events.py delete mode 100644 lms/djangoapps/courseware/features/lti.feature delete mode 100644 lms/djangoapps/courseware/features/lti.py delete mode 100644 lms/djangoapps/courseware/features/problems_setup.py delete mode 100644 lms/djangoapps/courseware/features/registration.py delete mode 100644 lms/djangoapps/instructor/features/__init__.py delete mode 100644 lms/djangoapps/instructor/features/common.py delete mode 100644 lms/djangoapps/instructor/features/data_download.py delete mode 100644 lms/envs/acceptance.py delete mode 100644 lms/envs/acceptance_docker.py delete mode 100644 openedx/core/lib/tests/tools.py delete mode 100644 pavelib/acceptance_test.py delete mode 100644 pavelib/utils/test/suites/acceptance_suite.py delete mode 100644 scripts/Jenkinsfiles/lettuce 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 "x2" in the code editor and press OK - And I save the page - Then the page text contains: - """ - x2 - """ - - 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: - """ -
    1. zzzz
        - """ - And I edit the page - Then the Raw Editor contains exactly: - """ -
      1. 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 d69768646bccddf31867b63dd9318040f143e3e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1842176 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⪙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 we can just fill text into) - # For this reason, we submit the initial code in the response - # (configured in the problem XML above) - pass - - elif problem_type == 'radio_text' or problem_type == 'checkbox_text': - - input_value = "8" if correctness == 'correct' else "5" - choice = "choiceinput_0bc" if correctness == 'correct' else "choiceinput_1bc" - world.css_fill( - inputfield( - course, - problem_type, - choice="choiceinput_0_numtolerance_input_0" - ), - input_value - ) - world.css_check(inputfield(course, problem_type, choice=choice)) - elif problem_type == 'image': - offset = 25 if correctness == "correct" else -25 - - def try_click(): - problem_html_loc = section_loc.course_key.make_usage_key('problem', 'image').html_id() - image_selector = "#imageinput_{}_2_1".format(problem_html_loc) - input_selector = "#input_{}_2_1".format(problem_html_loc) - - world.browser.execute_script('$("body").on("click", function(event) {console.log(event);})') # pylint: disable=unicode-format-string - initial_input = world.css_value(input_selector) - world.wait_for_visible(image_selector) - image = world.css_find(image_selector).first - (image.action_chains - .move_to_element(image._element) - .move_by_offset(offset, offset) - .click() - .perform()) - - world.wait_for(lambda _: world.css_value(input_selector) != initial_input) - - world.retry_on_exception(try_click) - - -def problem_has_answer(course, problem_type, answer_class): - if problem_type == "drop down": - if answer_class == 'blank': - assert world.is_css_not_present('option[selected="true"]') - else: - actual = world.css_value('option[selected="true"]') - expected = 'Option 2' if answer_class == 'correct' else 'Option 3' - assert actual == expected - - elif problem_type == "multiple choice": - if answer_class == 'correct': - assert_submitted(course, 'multiple choice', ['choice_2']) - elif answer_class == 'incorrect': - assert_submitted(course, 'multiple choice', ['choice_1']) - else: - assert_submitted(course, 'multiple choice', []) - - elif problem_type == "checkbox": - if answer_class == 'correct': - assert_submitted(course, 'checkbox', ['choice_0', 'choice_2']) - elif answer_class == 'incorrect': - assert_submitted(course, 'checkbox', ['choice_3']) - else: - assert_submitted(course, 'checkbox', []) - - elif problem_type == "radio": - if answer_class == 'correct': - assert_submitted(course, 'radio', ['choice_2']) - elif answer_class == 'incorrect': - assert_submitted(course, 'radio', ['choice_1']) - else: - assert_submitted(course, 'radio', []) - - elif problem_type == 'string': - if answer_class == 'blank': - expected = '' - else: - expected = 'correct string' if answer_class == 'correct' else 'incorrect' - assert_textfield(course, 'string', expected) - - elif problem_type == 'formula': - if answer_class == 'blank': - expected = '' - else: - expected = "x^2+2*x+y" if answer_class == 'correct' else 'x^2' - assert_textfield(course, 'formula', expected) - - elif problem_type in ("radio_text", "checkbox_text"): - if answer_class == 'blank': - expected = ('', '') - assert_choicetext_values(course, problem_type, (), expected) - elif answer_class == 'incorrect': - expected = ('5', '') - assert_choicetext_values(course, problem_type, ["choiceinput_1bc"], expected) - else: - expected = ('8', '') - assert_choicetext_values(course, problem_type, ["choiceinput_0bc"], expected) - - else: - # The other response types use random data, - # which would be difficult to check - # We trade input value coverage in the other tests for - # input type coverage in this test. - pass - - -def add_problem_to_course(course, problem_type, extra_meta=None): - ''' - Add a problem to the course we have created using factories. - ''' - - assert problem_type in PROBLEM_DICT - - # Generate the problem XML using capa.tests.response_xml_factory - factory_dict = PROBLEM_DICT[problem_type] - problem_xml = factory_dict['factory'].build_xml(**factory_dict['kwargs']) - metadata = {'rerandomize': 'always'} if 'metadata' not in factory_dict else factory_dict['metadata'] - if extra_meta: - metadata = dict(metadata, **extra_meta) - - # Create a problem item using our generated XML - # We set rerandomize=always in the metadata so that the "Reset" button - # will appear. - category_name = "problem" - return world.ItemFactory.create( - parent_location=section_location(course), - category=category_name, - display_name=str(problem_type), - data=problem_xml, - metadata=metadata - ) - - -def inputfield(course, problem_type, choice=None, input_num=1): - """ Return the css selector for `problem_type`. - For example, if problem_type is 'string', return - the text field for the string problem in the test course. - - `choice` is the name of the checkbox input in a group - of checkboxes. """ - - section_loc = section_location(course) - - ptype = problem_type.replace(" ", "_") - # this is necessary due to naming requirement for this problem type - if problem_type in ("radio_text", "checkbox_text"): - selector_template = "input#{}_2_{input}" - else: - selector_template = "input#input_{}_2_{input}" - - sel = selector_template.format( - section_loc.course_key.make_usage_key('problem', ptype).html_id(), - input=input_num, - ) - - if choice is not None: - base = "_choice_" if problem_type == "multiple choice" else "_" - sel = sel + base + str(choice) - - # If the input element doesn't exist, fail immediately - assert world.is_css_present(sel) - - # Retrieve the input element - return sel - - -def assert_submitted(course, problem_type, choices): - ''' - Assert that choice names given in *choices* are the only - ones submitted. - - Works for both radio and checkbox problems - ''' - - all_choices = ['choice_0', 'choice_1', 'choice_2', 'choice_3'] - for this_choice in all_choices: - def submit_problem(): - element = world.css_find(inputfield(course, problem_type, choice=this_choice)) - if this_choice in choices: - assert element.checked - else: - assert not element.checked - world.retry_on_exception(submit_problem) - - -def assert_textfield(course, problem_type, expected_text, input_num=1): - element_value = world.css_value(inputfield(course, problem_type, input_num=input_num)) - assert element_value == expected_text - - -def assert_choicetext_values(course, problem_type, choices, expected_values): - """ - Asserts that only the given choices are checked, and given - text fields have a desired value - """ - # Names of the radio buttons or checkboxes - all_choices = ['choiceinput_0bc', 'choiceinput_1bc'] - # Names of the numtolerance_inputs - all_inputs = [ - "choiceinput_0_numtolerance_input_0", - "choiceinput_1_numtolerance_input_0" - ] - for this_choice in all_choices: - element = world.css_find(inputfield(course, problem_type, choice=this_choice)) - - if this_choice in choices: - assert element.checked - else: - assert not element.checked - - for (name, expected) in zip(all_inputs, expected_values): - element = world.css_find(inputfield(course, problem_type, name)) - # Remove any trailing spaces that may have been added - assert element.value.strip() == expected diff --git a/lms/djangoapps/courseware/features/registration.py b/lms/djangoapps/courseware/features/registration.py deleted file mode 100644 index 87e557f19c..0000000000 --- a/lms/djangoapps/courseware/features/registration.py +++ /dev/null @@ -1,61 +0,0 @@ -# pylint: disable=missing-docstring - -import time - -from lettuce import step, world -from lettuce.django import django_url -from six import text_type - - -@step('I register for the course "([^"]*)"$') -def i_register_for_the_course(_step, course): - url = django_url('courses/%s/about' % text_type(world.scenario_dict['COURSE'].id)) - world.browser.visit(url) - world.css_click('.intro a.register') - assert world.is_css_present('.dashboard') - - -@step('I register to audit the course$') -def i_register_to_audit_the_course(_step): - url = django_url('courses/%s/about' % text_type(world.scenario_dict['COURSE'].id)) - world.browser.visit(url) - world.css_click('.intro a.register') - # When the page first loads some animation needs to - # complete before this button is in a stable location - world.retry_on_exception( - lambda: world.browser.find_by_name("honor_mode").click(), - max_attempts=10, - ignored_exceptions=AttributeError - ) - time.sleep(1) - assert world.is_css_present('.dashboard') - - -@step(u'I should see an empty dashboard message') -def i_should_see_empty_dashboard(_step): - empty_dash_css = '.empty-dashboard-message' - assert world.is_css_present(empty_dash_css) - - -@step(u'I should( NOT)? see the course numbered "([^"]*)" in my dashboard$') -def i_should_see_that_course_in_my_dashboard(_step, doesnt_appear, course): - course_link_css = '.my-courses a[href*="%s"]' % course - if doesnt_appear: - assert world.is_css_not_present(course_link_css) - else: - assert world.is_css_present(course_link_css) - - -@step(u'I unenroll from the course numbered "([^"]*)"') -def i_unenroll_from_that_course(_step, course): - more_actions_dropdown_link_selector = '[id*=actions-dropdown-link-0]' - assert world.is_css_present(more_actions_dropdown_link_selector) - world.css_click(more_actions_dropdown_link_selector) - - unregister_css = u'li.actions-item a.action-unenroll[data-course-number*="{course_number}"][href*=unenroll-modal]'.format(course_number=course) - assert world.is_css_present(unregister_css) - world.css_click(unregister_css) - - button_css = '#unenroll-modal input[value="Unenroll"]' - assert world.is_css_present(button_css) - world.css_click(button_css) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 10acbf9447..3441805245 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -23,6 +23,7 @@ from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.csrf import csrf_exempt from edx_django_utils.cache import RequestCache from edx_django_utils.monitoring import set_custom_metrics_for_course_key, set_monitoring_transaction_name +from edx_proctoring.api import get_attempt_status_summary from edx_proctoring.services import ProctoringService from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication from opaque_keys import InvalidKeyError @@ -257,28 +258,12 @@ def _add_timed_exam_info(user, course, section, section_context): settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) ) if section_is_time_limited: - # We need to import this here otherwise Lettuce test - # harness fails. When running in 'harvest' mode, the - # test service appears to get into trouble with - # circular references (not sure which as edx_proctoring.api - # doesn't import anything from edx-platform). Odd thing - # is that running: manage.py lms runserver --settings=acceptance - # works just fine, it's really a combination of Lettuce and the - # 'harvest' management command - # - # One idea is that there is some coupling between - # lettuce and the 'terrain' Djangoapps projects in /common - # This would need more investigation - from edx_proctoring.api import get_attempt_status_summary - - # # call into edx_proctoring subsystem # to get relevant proctoring information regarding this # level of the courseware # # This will return None, if (user, course_id, content_id) # is not applicable - # timed_exam_attempt_context = None try: timed_exam_attempt_context = get_attempt_status_summary( diff --git a/lms/djangoapps/instructor/features/__init__.py b/lms/djangoapps/instructor/features/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/lms/djangoapps/instructor/features/common.py b/lms/djangoapps/instructor/features/common.py deleted file mode 100644 index f191ba1a46..0000000000 --- a/lms/djangoapps/instructor/features/common.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -Define common steps for instructor dashboard acceptance tests. -""" - -# pylint: disable=missing-docstring -# pylint: disable=no-member -# pylint: disable=redefined-outer-name - -from __future__ import absolute_import - -from lettuce import step, world -from mock import patch - -from courseware.tests.factories import InstructorFactory, StaffFactory -from openedx.core.lib.tests.tools import assert_in # pylint: disable=no-name-in-module - - -@step(u'Given I am "([^"]*)" for a very large course') -def make_staff_or_instructor_for_large_course(step, role): - make_large_course(step, role) - - -@patch.dict('courseware.access.settings.FEATURES', {"MAX_ENROLLMENT_INSTR_BUTTONS": 0}) -def make_large_course(step, role): - i_am_staff_or_instructor(step, role) - - -@step(u'Given I am "([^"]*)" for a course') -def i_am_staff_or_instructor(step, role): # pylint: disable=unused-argument - ## In summary: makes a test course, makes a new Staff or Instructor user - ## (depending on `role`), and logs that user in to the course - - # Store the role - assert_in(role, ['instructor', 'staff']) - - # Clear existing courses to avoid conflicts - world.clear_courses() - - # Create a new course - course = world.CourseFactory.create( - org='edx', - number='999', - display_name='Test Course' - ) - - world.course_key = course.id - world.role = 'instructor' - # Log in as the an instructor or staff for the course - if role == 'instructor': - # Make & register an instructor for the course - world.instructor = InstructorFactory(course_key=world.course_key) - world.enroll_user(world.instructor, world.course_key) - - world.log_in( - username=world.instructor.username, - password='test', - email=world.instructor.email, - name=world.instructor.profile.name - ) - - else: - world.role = 'staff' - # Make & register a staff member - world.staff = StaffFactory(course_key=world.course_key) - world.enroll_user(world.staff, world.course_key) - - world.log_in( - username=world.staff.username, - password='test', - email=world.staff.email, - name=world.staff.profile.name - ) - - -def go_to_section(section_name): - # section name should be one of - # course_info, membership, student_admin, data_download, analytics, send_email - world.visit(u'/courses/{}'.format(world.course_key)) - world.css_click(u'a[href="/courses/{}/instructor"]'.format(world.course_key)) - world.css_click('[data-section="{0}"]'.format(section_name)) - - -@step(u'I click "([^"]*)"') -def click_a_button(step, button): # pylint: disable=unused-argument - - if button == "Generate Grade Report": - # Go to the data download section of the instructor dash - go_to_section("data_download") - - # Click generate grade report button - world.css_click('input[name="calculate-grades-csv"]') - - # Expect to see a message that grade report is being generated - expected_msg = "The grade report is being created." \ - " To view the status of the report, see" \ - " Pending Tasks below." - world.wait_for_visible('#report-request-response') - assert_in( - expected_msg, world.css_text('#report-request-response'), - msg="Could not find grade report generation success message." - ) - - elif button == "Grading Configuration": - # Go to the data download section of the instructor dash - go_to_section("data_download") - - world.css_click('input[name="dump-gradeconf"]') - - elif button == "List enrolled students' profile information": - # Go to the data download section of the instructor dash - go_to_section("data_download") - - world.css_click('input[name="list-profiles"]') - - elif button == "Download profile information as a CSV": - # Go to the data download section of the instructor dash - go_to_section("data_download") - - world.css_click('input[name="list-profiles-csv"]') - - else: - raise ValueError("Unrecognized button option " + button) - - -@step(u'I visit the "([^"]*)" tab') -def click_a_tab(step, tab_name): # pylint: disable=unused-argument - # course_info, membership, student_admin, data_download, analytics, send_email - tab_name_dict = { - 'Course Info': 'course_info', - 'Membership': 'membership', - 'Student Admin': 'student_admin', - 'Data Download': 'data_download', - 'Analytics': 'analytics', - 'Email': 'send_email', - } - go_to_section(tab_name_dict[tab_name]) diff --git a/lms/djangoapps/instructor/features/data_download.py b/lms/djangoapps/instructor/features/data_download.py deleted file mode 100644 index 3de43355b7..0000000000 --- a/lms/djangoapps/instructor/features/data_download.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -Define steps for instructor dashboard - data download tab -acceptance tests. -""" - -# pylint: disable=missing-docstring -# pylint: disable=no-member -# pylint: disable=redefined-outer-name - -from django.utils import http -from lettuce import step, world - -from terrain.steps import reload_the_page - -from openedx.core.lib.tests.tools import assert_in, assert_regexp_matches # pylint: disable=no-name-in-module - - -@step(u'I see a table of student profiles') -def find_student_profile_table(step): # pylint: disable=unused-argument - # Find the grading configuration display - world.wait_for_visible('#data-student-profiles-table') - - # Wait for the data table to be populated - world.wait_for(lambda _: world.css_text('#data-student-profiles-table') not in [u'', u'Loading']) - - if world.role == 'instructor': - expected_data = [ - world.instructor.username, - world.instructor.email, - world.instructor.profile.name, - world.instructor.profile.gender, - world.instructor.profile.goals - ] - elif world.role == 'staff': - expected_data = [ - world.staff.username, - world.staff.email, - world.staff.profile.name, - world.staff.profile.gender, - world.staff.profile.goals - ] - for datum in expected_data: - assert_in(datum, world.css_text('#data-student-profiles-table')) - - -@step(u"I do not see a button to 'List enrolled students' profile information'") -def no_student_profile_table(step): # pylint: disable=unused-argument - world.is_css_not_present('input[name="list-profiles"]') - - -@step(u"I see the grading configuration for the course") -def find_grading_config(step): # pylint: disable=unused-argument - # Find the grading configuration display - world.wait_for_visible('#data-grade-config-text') - # expected config is the default grading configuration from common/lib/xmodule/xmodule/course_module.py - expected_config = u"""----------------------------------------------------------------------------- -Course grader: - - -Graded sections: - subgrader=, type=Homework, category=Homework, weight=0.15 - subgrader=, type=Lab, category=Lab, weight=0.15 - subgrader=, type=Midterm Exam, category=Midterm Exam, weight=0.3 - subgrader=, type=Final Exam, category=Final Exam, weight=0.4 ------------------------------------------------------------------------------ -Listing grading context for course {} -graded sections: -[] -all graded blocks: -length=0""".format(world.course_key) - assert_in(expected_config, world.css_text('#data-grade-config-text')) - - -def verify_report_is_generated(report_name_substring): - # Need to reload the page to see the reports table updated - reload_the_page(step) - world.wait_for_visible('#report-downloads-table') - # Find table and assert a .csv file is present - quoted_id = http.urlquote(world.course_key).replace('/', '_') - expected_file_regexp = quoted_id + '_' + report_name_substring + r'_\d{4}-\d{2}-\d{2}-\d{4}\.csv' - assert_regexp_matches( - world.css_html('#report-downloads-table'), expected_file_regexp, - msg="Expected report filename was not found." - ) - - -@step(u"I see a grade report csv file in the reports table") -def find_grade_report_csv_link(step): # pylint: disable=unused-argument - verify_report_is_generated('grade_report') - - -@step(u"I see a student profile csv file in the reports table") -def find_student_profile_report_csv_link(step): # pylint: disable=unused-argument - verify_report_is_generated('student_profile_info') diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py deleted file mode 100644 index 15f050f955..0000000000 --- a/lms/envs/acceptance.py +++ /dev/null @@ -1,215 +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 -SITE_NAME = 'localhost:{}'.format(LETTUCE_SERVER_PORT) - -# Output Django logs to a file -import logging -logging.basicConfig(filename=TEST_ROOT / "log" / "lms_acceptance.log", level=logging.ERROR) - -# set root logger level -logging.getLogger().setLevel(logging.ERROR) - -import os -from random import choice - - -def seed(): - return os.getppid() - -# Silence noisy logs -LOG_OVERRIDES = [ - ('track.middleware', logging.CRITICAL), - ('codejail.safe_exec', logging.ERROR), - ('edx.courseware', logging.ERROR), - ('audit', logging.ERROR), - ('lms.djangoapps.instructor_task.api_helper', logging.ERROR), -] - -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={ - '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(), - } -} - -# Set this up so that 'paver lms --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", - }, - } -} - -TRACKING_BACKENDS.update({ - 'mongo': { - 'ENGINE': 'track.backends.mongodb.MongoBackend' - } -}) - -EVENT_TRACKING_BACKENDS['tracking_logs']['OPTIONS']['backends'].update({ - 'mongo': { - 'ENGINE': 'eventtracking.backends.mongodb.MongoBackend', - 'OPTIONS': { - 'database': 'track' - } - } -}) - - -BULK_EMAIL_DEFAULT_FROM_EMAIL = "test@test.org" - -# 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 - -# Use the auto_auth workflow for creating users and logging them in -FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True -FEATURES['RESTRICT_AUTOMATIC_AUTH'] = False - -# Enable third-party authentication -FEATURES['ENABLE_THIRD_PARTY_AUTH'] = True -THIRD_PARTY_AUTH = { - "Google": { - "SOCIAL_AUTH_GOOGLE_OAUTH2_KEY": "test", - "SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET": "test" - }, - "Facebook": { - "SOCIAL_AUTH_FACEBOOK_KEY": "test", - "SOCIAL_AUTH_FACEBOOK_SECRET": "test" - } -} - -# Enable fake payment processing page -FEATURES['ENABLE_PAYMENT_FAKE'] = True - -# Enable special exams -FEATURES['ENABLE_SPECIAL_EXAMS'] = True - -# Don't actually send any requests to Software Secure for student identity -# verification. -FEATURES['AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'] = True - -# 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 - -FEATURES['ENABLE_FEEDBACK_SUBMISSION'] = False - -# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command -INSTALLED_APPS.append('lettuce.django') -LETTUCE_APPS = ('courseware', 'instructor') - -# Lettuce appears to have a bug that causes it to search -# `instructor_task` when we specify the `instructor` app. -# This causes some pretty cryptic errors as lettuce tries -# to parse files in `instructor_task` as features. -# As a quick workaround, explicitly exclude the `instructor_task` app. -# The coursewarehistoryextended app also falls prey to this fuzzy -# for the courseware app. -LETTUCE_AVOID_APPS = ('instructor_task', 'coursewarehistoryextended') - -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, -} - - -##################################################################### -# See if the developer has any local overrides. -try: - from .private import * -except ImportError: - pass - -# Because an override for where to run will affect which ports to use, -# set these up after the local overrides. -# Configure XQueue interface to use our stub XQueue server -XQUEUE_INTERFACE = { - "url": "http://127.0.0.1:{0:d}".format(XQUEUE_PORT), - "django_auth": { - "username": "lms", - "password": "***REMOVED***" - }, - "basic_auth": ('anant', 'agarwal'), -} - -# 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 - -if FEATURES.get('ENABLE_COURSEWARE_SEARCH') or \ - FEATURES.get('ENABLE_DASHBOARD_SEARCH') or \ - FEATURES.get('ENABLE_COURSE_DISCOVERY'): - # Use MockSearchEngine as the search engine for test scenario - SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine" - -# 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 - -# We want to make sure that any new migrations are run -# see https://groups.google.com/forum/#!msg/django-developers/PWPj3etj3-U/kCl6pMsQYYoJ -MIGRATION_MODULES = {} diff --git a/lms/envs/acceptance_docker.py b/lms/envs/acceptance_docker.py deleted file mode 100644 index 6a9aa2da68..0000000000 --- a/lms/envs/acceptance_docker.py +++ /dev/null @@ -1,82 +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 * - -LETTUCE_HOST = os.environ['BOK_CHOY_HOSTNAME'] -SITE_NAME = '{}:{}'.format(LETTUCE_HOST, LETTUCE_SERVER_PORT) - -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={ - '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(), - } -} - -TRACKING_BACKENDS.update({ - 'mongo': { - 'ENGINE': 'track.backends.mongodb.MongoBackend', - 'OPTIONS': { - 'database': 'test', - 'collection': 'events', - 'host': [ - 'edx.devstack.mongo' - ], - 'port': 27017 - } - } -}) - -EVENT_TRACKING_BACKENDS['tracking_logs']['OPTIONS']['backends'].update({ - 'mongo': { - 'ENGINE': 'eventtracking.backends.mongodb.MongoBackend', - 'OPTIONS': { - 'database': 'track', - 'host': [ - 'edx.devstack.mongo' - ], - 'port': 27017 - } - } -}) - -# 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 -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/lms/envs/test.py b/lms/envs/test.py index 7960c91ad3..80754cf127 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -357,7 +357,6 @@ BLOCK_STRUCTURES_SETTINGS['PRUNING_ACTIVE'] = True # 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 @@ -551,8 +550,6 @@ COMPREHENSIVE_THEME_LOCALE_PATHS = [REPO_ROOT / "themes/conf/locale", ] LMS_ROOT_URL = "http://localhost:8000" -# TODO (felipemontoya): This key is only needed during lettuce tests. -# To be removed during https://openedx.atlassian.net/browse/DEPR-19 FRONTEND_LOGOUT_URL = LMS_ROOT_URL + '/logout' ECOMMERCE_API_URL = 'https://ecommerce.example.com/api/v2/' diff --git a/openedx/core/lib/tests/tools.py b/openedx/core/lib/tests/tools.py deleted file mode 100644 index 8ec7376c01..0000000000 --- a/openedx/core/lib/tests/tools.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Copy of the useful parts of nose.tools. This is only used for lettuce test -utility functions, which neither use pytest nor have access to a TestCase -instance. This module should be deleted once the last lettuce tests have -been ported over to bok-choy. - -Tracebacks should not descend into these functions. -We define the ``__unittest`` symbol in their module namespace so unittest will -skip them when printing tracebacks, just as it does for their corresponding -methods in ``unittest`` proper. -""" -from __future__ import absolute_import -import re -import unittest - -__all__ = [] - -# Use the same flag as unittest itself to prevent descent into these functions: -__unittest = 1 - -# Expose assert* from unittest.TestCase -# - give them pep8 style names -caps = re.compile('([A-Z])') - - -def pep8(name): - return caps.sub(lambda m: '_' + m.groups()[0].lower(), name) - - -class Dummy(unittest.TestCase): - def noop(self): - pass - - -_t = Dummy('noop') - -for at in [at for at in dir(_t) if at.startswith('assert') and '_' not in at]: - pepd = pep8(at) - vars()[pepd] = getattr(_t, at) - __all__.append(pepd) - -del Dummy -del _t -del pep8 diff --git a/pavelib/__init__.py b/pavelib/__init__.py index 9d5f4845ee..ee695d5593 100644 --- a/pavelib/__init__.py +++ b/pavelib/__init__.py @@ -2,6 +2,5 @@ paver commands """ from . import ( - assets, servers, docs, prereqs, quality, tests, js_test, i18n, bok_choy, - acceptance_test, database + assets, servers, docs, prereqs, quality, tests, js_test, i18n, bok_choy, database ) diff --git a/pavelib/acceptance_test.py b/pavelib/acceptance_test.py deleted file mode 100644 index afa24c56c9..0000000000 --- a/pavelib/acceptance_test.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -Acceptance test tasks -""" -from __future__ import print_function -from optparse import make_option - -from paver.easy import cmdopts, needs - -from pavelib.utils.passthrough_opts import PassthroughTask -from pavelib.utils.test.suites import AcceptanceTestSuite -from pavelib.utils.timer import timed - -try: - from pygments.console import colorize -except ImportError: - colorize = lambda color, text: text - -__test__ = False # do not collect - - -@needs( - 'pavelib.prereqs.install_prereqs', - 'pavelib.utils.test.utils.clean_reports_dir', -) -@cmdopts([ - ("system=", "s", "System to act on"), - ("default-store=", "m", "Default modulestore to use for course creation"), - ("fasttest", "a", "Run without collectstatic"), - make_option("--verbose", action="store_const", const=2, dest="verbosity"), - make_option("-q", "--quiet", action="store_const", const=0, dest="verbosity"), - make_option("-v", "--verbosity", action="count", dest="verbosity"), - ("default_store=", None, "deprecated in favor of default-store"), - ('extra_args=', 'e', 'deprecated, pass extra options directly in the paver commandline'), -]) -@PassthroughTask -@timed -def test_acceptance(options, passthrough_options): - """ - Run the acceptance tests for either lms or cms - """ - opts = { - 'fasttest': getattr(options, 'fasttest', False), - 'system': getattr(options, 'system', None), - 'default_store': getattr(options, 'default_store', None), - 'verbosity': getattr(options, 'verbosity', 3), - 'extra_args': getattr(options, 'extra_args', ''), - 'pdb': getattr(options, 'pdb', False), - 'passthrough_options': passthrough_options, - } - - if opts['system'] not in ['cms', 'lms']: - msg = colorize( - 'red', - 'No system specified, running tests for both cms and lms.' - ) - print(msg) - if opts['default_store'] not in ['draft', 'split']: - msg = colorize( - 'red', - 'No modulestore specified, running tests for both draft and split.' - ) - print(msg) - - suite = AcceptanceTestSuite(u'{} acceptance'.format(opts['system']), **opts) - suite.run() diff --git a/pavelib/utils/envs.py b/pavelib/utils/envs.py index 8e7a346403..8746d86992 100644 --- a/pavelib/utils/envs.py +++ b/pavelib/utils/envs.py @@ -88,9 +88,8 @@ class Env(object): BOK_CHOY_REPORT_DIR = BOK_CHOY_REPORT_DIR / shard_str BOK_CHOY_LOG_DIR = BOK_CHOY_LOG_DIR / shard_str - # For the time being, stubs are used by both the bok-choy and lettuce acceptance tests - # For this reason, the stubs package is currently located in the Django app called "terrain" - # where other lettuce configuration is stored. + # The stubs package is currently located in the Django app called "terrain" + # from when they were used by both the bok-choy and lettuce (deprecated) acceptance tests BOK_CHOY_STUB_DIR = REPO_ROOT / "common" / "djangoapps" / "terrain" # Directory that videos are served from diff --git a/pavelib/utils/test/suites/__init__.py b/pavelib/utils/test/suites/__init__.py index 70950d7ae0..9a7bcace73 100644 --- a/pavelib/utils/test/suites/__init__.py +++ b/pavelib/utils/test/suites/__init__.py @@ -5,5 +5,4 @@ from .suite import TestSuite from .pytest_suite import PytestSuite, SystemTestSuite, LibTestSuite from .python_suite import PythonTestSuite from .js_suite import JsTestSuite, JestSnapshotTestSuite -from .acceptance_suite import AcceptanceTestSuite from .bokchoy_suite import BokChoyTestSuite, Pa11yCrawler diff --git a/pavelib/utils/test/suites/acceptance_suite.py b/pavelib/utils/test/suites/acceptance_suite.py deleted file mode 100644 index f1029dbec6..0000000000 --- a/pavelib/utils/test/suites/acceptance_suite.py +++ /dev/null @@ -1,161 +0,0 @@ -""" -Acceptance test suite -""" -from os import environ -from paver.easy import sh, call_task, task -from pavelib.utils.test import utils as test_utils -from pavelib.utils.test.suites.suite import TestSuite -from pavelib.utils.envs import Env -from pavelib.utils.timer import timed - -__test__ = False # do not collect - - -DBS = { - 'default': Env.REPO_ROOT / 'test_root/db/test_edx.db', - 'student_module_history': Env.REPO_ROOT / 'test_root/db/test_student_module_history.db' -} -DB_CACHES = { - 'default': Env.REPO_ROOT / 'common/test/db_cache/lettuce.db', - 'student_module_history': Env.REPO_ROOT / 'common/test/db_cache/lettuce_student_module_history.db' -} - - -@task -@timed -def setup_acceptance_db(): - """ - TODO: Improve the following - - Since the CMS depends on the existence of some database tables - that are now in common but used to be in LMS (Role/Permissions for Forums) - we need to create/migrate the database tables defined in the LMS. - We might be able to address this by moving out the migrations from - lms/django_comment_client, but then we'd have to repair all the existing - migrations from the upgrade tables in the DB. - But for now for either system (lms or cms), use the lms - definitions to sync and migrate. - """ - - for db in DBS: - if DBS[db].isfile(): - # Since we are using SQLLite, we can reset the database by deleting it on disk. - DBS[db].remove() - - settings = 'acceptance_docker' if Env.USING_DOCKER else 'acceptance' - if all(DB_CACHES[cache].isfile() for cache in DB_CACHES): - # To speed up migrations, we check for a cached database file and start from that. - # The cached database file should be checked into the repo - - # Copy the cached database to the test root directory - for db_alias in DBS: - sh(u"cp {db_cache} {db}".format(db_cache=DB_CACHES[db_alias], db=DBS[db_alias])) - - # Run migrations to update the db, starting from its cached state - for db_alias in sorted(DBS): - # pylint: disable=line-too-long - sh(u"./manage.py lms --settings {} migrate --traceback --noinput --fake-initial --database {}".format(settings, db_alias)) - sh(u"./manage.py cms --settings {} migrate --traceback --noinput --fake-initial --database {}".format(settings, db_alias)) - else: - # If no cached database exists, migrate then create the cache - for db_alias in sorted(DBS.keys()): - sh(u"./manage.py lms --settings {} migrate --traceback --noinput --database {}".format(settings, db_alias)) - sh(u"./manage.py cms --settings {} migrate --traceback --noinput --database {}".format(settings, db_alias)) - - # Create the cache if it doesn't already exist - for db_alias in DBS.keys(): - sh(u"cp {db} {db_cache}".format(db_cache=DB_CACHES[db_alias], db=DBS[db_alias])) - - -class AcceptanceTest(TestSuite): - """ - A class for running lettuce acceptance tests. - """ - def __init__(self, *args, **kwargs): - super(AcceptanceTest, self).__init__(*args, **kwargs) - self.report_dir = Env.REPORT_DIR / 'acceptance' - self.fasttest = kwargs.get('fasttest', False) - self.system = kwargs.get('system') - self.default_store = kwargs.get('default_store') - self.extra_args = kwargs.get('extra_args', '') - self.settings = 'acceptance_docker' if Env.USING_DOCKER else 'acceptance' - - def __enter__(self): - super(AcceptanceTest, self).__enter__() - self.report_dir.makedirs_p() - if not self.fasttest: - self._update_assets() - - def __exit__(self, exc_type, exc_value, traceback): - super(AcceptanceTest, self).__exit__(exc_type, exc_value, traceback) - test_utils.clean_mongo() - - @property - def cmd(self): - - lettuce_host = ['LETTUCE_HOST={}'.format(Env.SERVER_HOST)] if Env.USING_DOCKER else [] - report_file = self.report_dir / "{}.xml".format(self.system) - report_args = [u"--xunit-file {}".format(report_file)] - return lettuce_host + [ - # set DBUS_SESSION_BUS_ADDRESS to avoid hangs on Chrome - "DBUS_SESSION_BUS_ADDRESS=/dev/null", - "DEFAULT_STORE={}".format(self.default_store), - "./manage.py", - self.system, - "--settings={}".format(self.settings), - "harvest", - "--traceback", - "--debug-mode", - "--verbosity={}".format(self.verbosity), - ] + report_args + [ - self.extra_args - ] + self.passthrough_options - - def _update_assets(self): - """ - Internal helper method to manage asset compilation - """ - args = [self.system, '--settings={}'.format(self.settings)] - call_task('pavelib.assets.update_assets', args=args) - - -class AcceptanceTestSuite(TestSuite): - """ - A class for running lettuce acceptance tests. - """ - def __init__(self, *args, **kwargs): - super(AcceptanceTestSuite, self).__init__(*args, **kwargs) - self.root = 'acceptance' - self.fasttest = kwargs.get('fasttest', False) - - # Set the environment so that webpack understands where to compile its resources. - # This setting is expected in other environments, so we are setting it for the - # bok-choy test run. - environ['EDX_PLATFORM_SETTINGS'] = 'test_static_optimized' - - if kwargs.get('system'): - systems = [kwargs['system']] - else: - systems = ['lms', 'cms'] - - if kwargs.get('default_store'): - stores = [kwargs['default_store']] - else: - # TODO fix Acceptance tests with Split (LMS-11300) - # stores = ['split', 'draft'] - stores = ['draft'] - - self.subsuites = [] - for system in systems: - for default_store in stores: - kwargs['system'] = system - kwargs['default_store'] = default_store - self.subsuites.append(AcceptanceTest(u'{} acceptance using {}'.format(system, default_store), **kwargs)) - - def __enter__(self): - super(AcceptanceTestSuite, self).__enter__() - if not (self.fasttest or self.skip_clean): - test_utils.clean_test_files() - - if not self.fasttest: - setup_acceptance_db() diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 572b752023..8338fbe8bc 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -28,9 +28,6 @@ mysqlclient<1.5 # Can be removed when we get to Python 3. pylint-plugin-utils==0.3 -# Browser driver used by lettuce - pinned because splinter==0.10.0 breaks lettuce tests. EDUCATOR-3795 -splinter==0.9.0 - # transifex-client 0.13.6 requires urllib3<1.24, but requests will pull in urllib3==1.24 (https://github.com/transifex/transifex-client/pull/241/files) urllib3<1.24 diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 9df898f572..6d741701ab 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -112,7 +112,7 @@ edx-django-release-util==0.3.1 edx-django-sites-extensions==2.3.1 edx-django-utils==1.0.3 edx-drf-extensions==2.2.0 -edx-enterprise==1.4.6 +edx-enterprise==1.4.7 edx-i18n-tools==0.4.8 edx-milestones==0.1.13 edx-oauth2-provider==1.2.2 diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 78ae2d892d..5baa9b5aed 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -21,7 +21,6 @@ git+https://github.com/edx/django-rest-framework.git@1ceda7c086fddffd1c440cc8685 -e git+https://github.com/edx/DoneXBlock.git@01a14f3bd80ae47dd08cdbbe2f88f3eb88d00fba#egg=done-xblock -e git+https://github.com/jazkarta/edx-jsme.git@690dbf75441fa91c7c4899df0b83d77f7deb5458#egg=edx-jsme git+https://github.com/mitodl/edx-sga.git@3828ba9e413080a81b907a3381e5ffa05e063f81#egg=edx-sga==0.8.3 -git+https://github.com/edx/lettuce.git@7a04591c78ac56dac3eb3e91ca94b15cce844133#egg=lettuce==0.2.23+edx.1 git+https://github.com/edx/xblock-lti-consumer.git@v1.1.8#egg=lti_consumer-xblock==1.1.8 git+https://github.com/edx/MongoDBProxy.git@25b99097615bda06bd7cdfe5669ed80dc2a7fed0#egg=MongoDBProxy==0.1.0 -e . @@ -134,7 +133,7 @@ edx-django-release-util==0.3.1 edx-django-sites-extensions==2.3.1 edx-django-utils==1.0.3 edx-drf-extensions==2.2.0 -edx-enterprise==1.4.6 +edx-enterprise==1.4.7 edx-i18n-tools==0.4.8 edx-lint==1.1.2 edx-milestones==0.1.13 @@ -155,13 +154,11 @@ entrypoints==0.3 enum34==1.1.6 event-tracking==0.2.8 execnet==1.6.0 -extras==1.0.0 factory_boy==2.8.1 faker==1.0.5 feedparser==5.1.3 filelock==3.0.10 firebase-token-generator==1.3.2 -fixtures==3.0.0 flake8-polyfill==1.0.2 flake8==3.7.7 flask==1.0.2 @@ -172,7 +169,6 @@ funcsigs==1.0.2 functools32==3.2.3.post2 ; python_version == "2.7" future==0.17.1 futures==3.2.0 ; python_version == "2.7" -fuzzywuzzy==0.17.0 geoip2==2.9.0 glob2==0.6 gunicorn==19.0 @@ -199,7 +195,6 @@ lazy-object-proxy==1.3.1 lazy==1.1 lepl==5.1.3 libsass==0.10.0 -linecache2==1.0.0 loremipsum==1.0.5 lxml==3.8.0 mailsnake==1.6.4 @@ -279,10 +274,8 @@ pytest==4.4.1 python-dateutil==2.4.0 python-levenshtein==0.12.0 python-memcached==1.59 -python-mimeparse==1.6.0 python-openid==2.2.5 ; python_version == "2.7" python-slugify==1.2.6 -python-subunit==1.3.0 python-swiftclient==3.7.0 python3-saml==1.5.0 pytz==2019.1 @@ -320,25 +313,20 @@ sortedcontainers==2.1.0 soupsieve==1.9.1 sphinx==1.8.5 sphinxcontrib-websupport==1.1.0 # via sphinx -splinter==0.9.0 sqlparse==0.3.0 stevedore==1.30.1 -sure==1.4.11 sympy==1.4 testfixtures==6.7.0 -testtools==2.3.0 text-unidecode==1.2 tincan==0.0.5 toml==0.10.0 tox-battery==0.5.1 tox==3.9.0 -traceback2==1.4.0 transifex-client==0.13.6 twisted==19.2.0 typing==3.6.6 unicodecsv==0.14.1 unidecode==1.0.23 -unittest2==1.1.0 uritemplate==3.0.0 urllib3==1.23 urlobject==2.4.3 diff --git a/requirements/edx/testing.in b/requirements/edx/testing.in index 45aecbb6f7..e299932742 100644 --- a/requirements/edx/testing.in +++ b/requirements/edx/testing.in @@ -45,11 +45,7 @@ pytest-xdist # Parallel execution of tests on multiple CPU cores or radon # Calculates cyclomatic complexity of Python code (code quality utility) selenium # Browser automation library, used for acceptance tests singledispatch # Backport of functools.singledispatch from Python 3.4+, used in tests of XBlock rendering -splinter # Browser driver used by lettuce testfixtures # Provides a LogCapture utility used by several tests tox # virtualenv management for tests tox-battery # Makes tox aware of requirements file changes transifex-client # Command-line interface for the Transifex localization service - -# Deprecated acceptance testing framework --e git+https://github.com/edx/lettuce.git@7a04591c78ac56dac3eb3e91ca94b15cce844133#egg=lettuce==0.2.23+edx.1 diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 3b33268bc3..336c9e61bb 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -20,7 +20,6 @@ git+https://github.com/edx/django-rest-framework.git@1ceda7c086fddffd1c440cc8685 -e git+https://github.com/edx/DoneXBlock.git@01a14f3bd80ae47dd08cdbbe2f88f3eb88d00fba#egg=done-xblock -e git+https://github.com/jazkarta/edx-jsme.git@690dbf75441fa91c7c4899df0b83d77f7deb5458#egg=edx-jsme git+https://github.com/mitodl/edx-sga.git@3828ba9e413080a81b907a3381e5ffa05e063f81#egg=edx-sga==0.8.3 -git+https://github.com/edx/lettuce.git@7a04591c78ac56dac3eb3e91ca94b15cce844133#egg=lettuce==0.2.23+edx.1 git+https://github.com/edx/xblock-lti-consumer.git@v1.1.8#egg=lti_consumer-xblock==1.1.8 git+https://github.com/edx/MongoDBProxy.git@25b99097615bda06bd7cdfe5669ed80dc2a7fed0#egg=MongoDBProxy==0.1.0 -e . @@ -44,7 +43,7 @@ anyjson==0.3.3 apipkg==1.5 # via execnet appdirs==1.4.3 argh==0.26.2 -argparse==1.4.0 # via caniusepython3, unittest2 +argparse==1.4.0 # via caniusepython3 asn1crypto==0.24.0 astroid==1.5.3 # via pylint, pylint-celery atomicwrites==1.3.0 # via pytest @@ -130,7 +129,7 @@ edx-django-release-util==0.3.1 edx-django-sites-extensions==2.3.1 edx-django-utils==1.0.3 edx-drf-extensions==2.2.0 -edx-enterprise==1.4.6 +edx-enterprise==1.4.7 edx-i18n-tools==0.4.8 edx-lint==1.1.2 edx-milestones==0.1.13 @@ -150,13 +149,11 @@ entrypoints==0.3 # via flake8 enum34==1.1.6 event-tracking==0.2.8 execnet==1.6.0 # via pytest-xdist -extras==1.0.0 # via python-subunit, testtools factory_boy==2.8.1 faker==1.0.5 # via factory-boy feedparser==5.1.3 filelock==3.0.10 # via tox firebase-token-generator==1.3.2 -fixtures==3.0.0 # via testtools flake8-polyfill==1.0.2 # via radon flake8==3.7.7 # via flake8-polyfill flask==1.0.2 # via moto @@ -167,7 +164,6 @@ funcsigs==1.0.2 # via pytest functools32==3.2.3.post2 ; python_version == "2.7" # via flake8, parsel future==0.17.1 futures==3.2.0 ; python_version == "2.7" -fuzzywuzzy==0.17.0 geoip2==2.9.0 glob2==0.6 gunicorn==19.0 @@ -193,7 +189,6 @@ lazy-object-proxy==1.3.1 # via astroid lazy==1.1 lepl==5.1.3 libsass==0.10.0 -linecache2==1.0.0 # via traceback2 loremipsum==1.0.5 lxml==3.8.0 mailsnake==1.6.4 @@ -270,10 +265,8 @@ pytest==4.4.1 python-dateutil==2.4.0 python-levenshtein==0.12.0 python-memcached==1.59 -python-mimeparse==1.6.0 # via testtools python-openid==2.2.5 ; python_version == "2.7" python-slugify==1.2.6 # via code-annotations, transifex-client -python-subunit==1.3.0 python-swiftclient==3.7.0 python3-saml==1.5.0 pytz==2019.1 @@ -307,25 +300,20 @@ social-auth-core==1.7.0 sorl-thumbnail==12.3 sortedcontainers==2.1.0 soupsieve==1.9.1 -splinter==0.9.0 sqlparse==0.3.0 stevedore==1.30.1 -sure==1.4.11 sympy==1.4 testfixtures==6.7.0 -testtools==2.3.0 # via fixtures, python-subunit text-unidecode==1.2 # via faker tincan==0.0.5 toml==0.10.0 # via tox tox-battery==0.5.1 tox==3.9.0 -traceback2==1.4.0 # via testtools, unittest2 transifex-client==0.13.6 twisted==19.2.0 # via scrapy typing==3.6.6 # via flake8 unicodecsv==0.14.1 unidecode==1.0.23 # via python-slugify -unittest2==1.1.0 # via testtools uritemplate==3.0.0 urllib3==1.23 urlobject==2.4.3 # via pa11ycrawler diff --git a/scripts/Jenkinsfiles/lettuce b/scripts/Jenkinsfiles/lettuce deleted file mode 100644 index a61f7f934b..0000000000 --- a/scripts/Jenkinsfiles/lettuce +++ /dev/null @@ -1,151 +0,0 @@ -def runLettuceTests() { - // Determine git refspec, branch, and clone type - if (env.ghprbActualCommit) { - git_branch = "${ghprbActualCommit}" - git_refspec = "+refs/pull/${ghprbPullId}/*:refs/remotes/origin/pr/${ghprbPullId}/*" - } else { - git_branch = "${BRANCH_NAME}" - git_refspec = "+refs/heads/${BRANCH_NAME}:refs/remotes/origin/${BRANCH_NAME}" - } - sshagent(credentials: ['jenkins-worker', 'jenkins-worker-pem'], ignoreMissing: true) { - checkout changelog: false, poll: false, scm: [$class: 'GitSCM', branches: [[name: git_branch]], - doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'CloneOption', honorRefspec: true, - noTags: true, shallow: true]], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'jenkins-worker', - refspec: git_refspec, url: "git@github.com:edx/${REPO_NAME}.git"]]] - sh 'bash scripts/all-tests.sh' - } -} - -def lettuceTestCleanup() { - archiveArtifacts allowEmptyArchive: true, artifacts: 'test_root/log/**/*.log,*.log' - junit '**/reports/acceptance/*.xml' -} - -pipeline { - agent { label "jenkins-worker" } - options { - timestamps() - timeout(60) - } - stages { - stage('Mark build as pending on Github') { - when { - // Only run github-build-status for master builds - expression { env.ghprbActualCommit == null } - } - steps { - script { - commit_sha = sh(returnStdout: true, script: 'git rev-parse HEAD').trim() - build job: 'github-build-status', - parameters: [ - string(name: 'GIT_SHA', value: commit_sha), - string(name: 'GITHUB_ORG', value: 'edx'), - string(name: 'GITHUB_REPO', value: "${REPO_NAME}"), - string(name: 'TARGET_URL', value: "${BUILD_URL}"), - string(name: 'DESCRIPTION', value: 'Pending'), - string(name: 'CONTEXT', value: "${GITHUB_CONTEXT}"), - string(name: 'CREATE_DEPLOYMENT', value: 'false'), - string(name: 'BUILD_STATUS', value: 'pending') - ], - propagate: false, wait: false - } - } - } - stage('Run Tests') { - parallel { - stage("lms-acceptance") { - agent { label "jenkins-worker" } - environment { - TEST_SUITE = "lms-acceptance" - } - steps { - script { - runLettuceTests() - } - } - post { - always { - script { - lettuceTestCleanup() - } - } - } - } - stage("cms-acceptance") { - agent { label "jenkins-worker" } - environment { - TEST_SUITE = "cms-acceptance" - } - steps { - script { - runLettuceTests() - } - } - post { - always { - script { - lettuceTestCleanup() - } - } - } - } - } - } - } - post { - always { - script{ - if (env.ghprbPullId != null) { - // For PR jobs, run the edx-platform-test-notifier for PR reporting - build job: 'edx-platform-test-notifier', parameters: [string(name: 'PR_NUMBER', value: "${ghprbPullId}")], wait: false - } else { - // For master jobs run github-build-status and report to slack when necessary - if (currentBuild.currentResult == "SUCCESS") { - create_deployment = "true" - build_status = "success" - build_description = "Build Passed" - } - else { - create_deployment = "false" - build_status = "failure" - build_description = "Build Failed" - } - - commit_sha = sh(returnStdout: true, script: 'git rev-parse HEAD').trim() - build job: 'github-build-status', - parameters: [ - string(name: 'GIT_SHA', value: commit_sha), - string(name: 'GITHUB_ORG', value: 'edx'), - string(name: 'GITHUB_REPO', value: "${REPO_NAME}"), - string(name: 'TARGET_URL', value: "${BUILD_URL}"), - string(name: 'DESCRIPTION', value: build_description), - string(name: 'CONTEXT', value: "${GITHUB_CONTEXT}"), - string(name: 'CREATE_DEPLOYMENT', value: create_deployment), - string(name: 'BUILD_STATUS', value: build_status) - ], - propagate: false, wait: false - - if (currentBuild.currentResult != "SUCCESS"){ - slackSend "`${JOB_NAME}` #${BUILD_NUMBER}: ${currentBuild.currentResult} after ${currentBuild.durationString.replace(' and counting', '')}\n${BUILD_URL}" - - email_body = "See: <${BUILD_URL}>\n\nChanges:\n" - change_sets = currentBuild.changeSets - for (int j = 0; j < change_sets.size(); j++) { - change_set_items = change_sets[j].items - for (int k = 0; k < change_set_items.length; k++) { - item = change_set_items[k] - email_body = email_body + "\n Commit: ${item.commitId} by ${item.author}: ${item.msg}" - } - } - emailext body: email_body, - subject: "Build failed in Jenkins: ${JOB_NAME} #${BUILD_NUMBER}", to: 'testeng@edx.org' - } else if (currentBuild.currentResult == "SUCCESS" && currentBuild.previousBuild.currentResult != "SUCCESS") { - slackSend "`${JOB_NAME}` #${BUILD_NUMBER}: Back to normal after ${currentBuild.durationString.replace(' and counting', '')}\n${BUILD_URL}" - emailext body: "See <${BUILD_URL}>", - subject: "Jenkins Build is back to normal: ${JOB_NAME} #${BUILD_NUMBER}", to: 'testeng@edx.org' - } - } - } - } - } -} diff --git a/scripts/dependencies/testing.py b/scripts/dependencies/testing.py index 8d98bc2822..dfd4409d92 100755 --- a/scripts/dependencies/testing.py +++ b/scripts/dependencies/testing.py @@ -25,8 +25,6 @@ pattern_fragments = [ r'/testutils\.py', # testutils.py r'/tests$', # tests/__init__.py r'conftest\.py', # conftest.py - r'/envs/acceptance\.py', # cms/envs/acceptance.py, lms/envs/acceptance.py - r'/envs/acceptance_docker\.py', # cms/envs/acceptance.py, lms/envs/acceptance.py r'/factories\.py', # factories.py r'^terrain', # terrain/* r'/setup_models_to_send_test_emails\.py', # setup_models_to_send_test_emails management command diff --git a/scripts/generic-ci-tests.sh b/scripts/generic-ci-tests.sh index 5077b33887..90a9dc17b2 100755 --- a/scripts/generic-ci-tests.sh +++ b/scripts/generic-ci-tests.sh @@ -20,10 +20,6 @@ set -e # - "commonlib-unit": Run Python unit tests from the common/lib directory # - "commonlib-js-unit": Run the JavaScript tests and the Python unit # tests from the common/lib directory -# - "lms-acceptance": Run the acceptance (Selenium/Lettuce) tests for -# the LMS -# - "cms-acceptance": Run the acceptance (Selenium/Lettuce) tests for -# Studio # - "bok-choy": Run acceptance tests that use the bok-choy framework # # `SHARD` is a number indicating which subset of the tests to build. @@ -176,14 +172,6 @@ case "$TEST_SUITE" in exit $EXIT ;; - "lms-acceptance") - $TOX paver test_acceptance -s lms -vvv --with-xunit - ;; - - "cms-acceptance") - $TOX paver test_acceptance -s cms -vvv --with-xunit - ;; - "bok-choy") PAVER_ARGS="-n $NUMBER_OF_BOKCHOY_THREADS"