diff --git a/cms/djangoapps/contentstore/features/__init__.py b/cms/djangoapps/contentstore/features/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/cms/djangoapps/contentstore/features/advanced_settings.py b/cms/djangoapps/contentstore/features/advanced_settings.py deleted file mode 100644 index 63d63d37612f387c87596ffd108b15f06dfbc588..0000000000000000000000000000000000000000 --- 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 845d0df56c482d2b094a74c29f8b3a3a167242b8..0000000000000000000000000000000000000000 --- 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 0194f1ca33908ef4edd7b1068285a7f397b2635f..0000000000000000000000000000000000000000 --- 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': '<h3 class="hd hd-2">Announcement Date</h3>', - 'Zooming Image Tool': '<h3 class="hd hd-2">Zooming Image Tool</h3>', - 'E-text Written in LaTeX': '<h3 class="hd hd-2">Example: E-text page</h3>', - 'Raw HTML': '<p>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 a066fb145f91bc4fd4c6c414b11b0292c168f811..0000000000000000000000000000000000000000 --- 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 54fb036ef621c7f9a9bf9c6937b2e2148fbc42ea..0000000000000000000000000000000000000000 --- 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, "<h1>Overview</h1>") - - -############### 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 e75db207bc07bc9845644d6ee7f6f42f1201a00f..0000000000000000000000000000000000000000 --- 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 7b7147167971fb8f2a641c2495fc6138d09b795a..0000000000000000000000000000000000000000 --- 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 "<p class='title'>pages</p><style><!-- .title { color: red; } --></style>" in the code editor and press OK - And I save the page - Then the page text contains: - """ - <p class="title">pages</p> - <style><!-- - .title { color: red; } - --></style> - """ - - Scenario: TinyMCE and CodeMirror preserve span tags - Given I have created a Blank HTML Page - When I edit the page - And type "<span>Test</span>" in the code editor and press OK - And I save the page - Then the page text contains: - """ - <span>Test</span> - """ - - Scenario: TinyMCE and CodeMirror preserve math tags - Given I have created a Blank HTML Page - When I edit the page - And type "<math><msup><mi>x</mi><mn>2</mn></msup></math>" in the code editor and press OK - And I save the page - Then the page text contains: - """ - <math><msup><mi>x</mi><mn>2</mn></msup></math> - """ - - 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 "<img src="/static/image.jpg">" in the code editor and press OK - Then the src link is rewritten to the asset link "image.jpg" - And the code editor displays "<p><img src="/static/image.jpg" /></p>" - - 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: - """ - <p><code>display as code</code></p> - """ - - Scenario: Raw HTML component does not change text - Given I have created a raw HTML component - When I edit the page - And type "<li>zzzz<ol>" into the Raw Editor - And I save the page - Then the page text contains: - """ - <li>zzzz<ol> - """ - And I edit the page - Then the Raw Editor contains exactly: - """ - <li>zzzz<ol> - """ - - 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 490e7a0efb35bcbef33e9366095fd469b20fadee..0000000000000000000000000000000000000000 --- 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 a239d109012ebfe0b6c307ef59c656f6b3f7f876..0000000000000000000000000000000000000000 --- 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 "<script>alert('test')</script>" - - # 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 72b46a733bf7e78e954ca34c7c5a732e2649b6a7..0000000000000000000000000000000000000000 --- 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, "<script>alert('test')</script>") - 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 = """ - <problem> - <annotationresponse> - <annotationinput><text>Text of annotation</text></annotationinput> - </annotationresponse> - </problem>""" - 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, "<script>alert('test')</script>", 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 e0b2ca9349dbf75913317ae21f8de02fbc22eb0c..0000000000000000000000000000000000000000 --- 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 a37cf8d1700741b7e1509b08173c48ed1619b727..0000000000000000000000000000000000000000 --- 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 b00a94db5f750a16e5dd91ee84c9ee9af8e39f3c..0000000000000000000000000000000000000000 --- 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 a4592d6a7bc5b7ae0e202a2120e27accd15adbb8..6b64990c9f7df25bf10980bb0d6dfbfa26c4c40a 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 b769c8414c9996e258f1522b93acfe85b99b5f77..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 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 455ba2cf251ad8568b994d58eccdd5802f77bdbb..0000000000000000000000000000000000000000 --- 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 94457a44fd91d58dfb4be5414cb62ecfc32aa2de..0000000000000000000000000000000000000000 --- 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 8df3f354d876261a79736b03bd7a3d84f4899ce3..0000000000000000000000000000000000000000 --- 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 62ade0357521f8b078fa726fe49bdbc6013a1628..0000000000000000000000000000000000000000 --- 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<server>[^_]+)') - 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 e4114ff6b994667e3d7b554fd86c49595f68eaaf..0000000000000000000000000000000000000000 --- 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 57f1a987415adc6ec4d860216dbddc0b6200b487..0000000000000000000000000000000000000000 --- 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 14d9af35635795a9d5a01690fd4ef139721d45db..af0fd9bdf501b6a1aa5473819bb423c48cc0587a 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 bcac5933ed5ad59bab19b9df8076a8757aa6daa6..7bc35f8e3e6a82dba4f9c1a1d4f21e900746d1f4 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 Binary files a/common/test/db_cache/lettuce.db and /dev/null differ diff --git a/common/test/db_cache/lettuce_student_module_history.db b/common/test/db_cache/lettuce_student_module_history.db deleted file mode 100644 index 20b9ae9bc68fcd49414a0c2fcbe1c675ef80378c..0000000000000000000000000000000000000000 Binary files a/common/test/db_cache/lettuce_student_module_history.db and /dev/null differ diff --git a/docs/testing.rst b/docs/testing.rst index 6942682513aaad427db92beec12309068a9f9939..8848e7e8e6296509e4811872592dfd32f73db695 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -68,13 +68,7 @@ UI Acceptance Tests - We use `Bok Choy`_ to write end-user acceptance tests directly in Python, using the framework to maximize reliability and maintainability. -- We used to use `lettuce`_ to write BDD-style tests but it's now deprecated - in favor of Bok Choy for new tests. Most of these tests simulate user - interactions through the browser using `splinter`_. - .. _Bok Choy: https://bok-choy.readthedocs.org/en/latest/tutorial.html -.. _lettuce: http://lettuce.it/ -.. _splinter: http://splinter.cobrateam.info/ Internationalization @@ -101,8 +95,6 @@ Test Locations - Set up and helper methods, and stubs for external services: ``common/djangoapps/terrain`` - - Lettuce Tests: located in ``features`` subpackage within a Django - app. For example: ``lms/djangoapps/courseware/features`` - Bok Choy Acceptance Tests: located under ``common/test/acceptance/tests`` - Bok Choy Accessibility Tests: located under ``common/test/acceptance/tests`` and tagged with ``@attr("a11y")`` - Bok Choy PageObjects: located under ``common/test/acceptance/pages`` @@ -431,8 +423,7 @@ Object and Promise design patterns. These prerequisites are all automatically installed and available in `Devstack`_, the supported development enviornment for the Open edX platform. -* Chromedriver and Chrome (see `Running Lettuce Acceptance Tests`_ below for - the latest tested versions) +* Chromedriver and Chrome * Mongo @@ -591,65 +582,6 @@ You must run BOTH `--testsonly` and `--fasttest`. Control-C. *Warning*: Only hit Control-C one time so the pytest framework can properly clean up. -Running Lettuce Acceptance Tests -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Although it's deprecated now `lettuce`_ acceptance tests still exists in the -code base. Most of our tests use `Splinter`_ to simulate UI browser -interactions. Splinter, in turn, uses `Selenium`_ to control the Chrome -browser. - -**Prerequisite**: You must have `ChromeDriver`_ installed to run the tests in -Chrome. The tests are confirmed to run with Chrome (not Chromium) version -34.0.1847.116 with ChromeDriver version 2.6.232917. - -.. _ChromeDriver: https://code.google.com/p/selenium/wiki/ChromeDriver - -To run all the acceptance tests, run this command:: - - paver test_acceptance - -To run only for lms or cms, run one of these commands:: - - paver test_acceptance -s lms - paver test_acceptance -s cms - -For example, this command tests only a specific feature:: - - paver test_acceptance -s lms --extra_args="lms/djangoapps/courseware/features/problems.feature" - -A command like this tests only a specific scenario:: - - paver test_acceptance -s lms --extra_args="lms/djangoapps/courseware/features/problems.feature -s 3" - -To start the debugger on failure, pass the ``--pdb`` option to the paver command like this:: - - paver test_acceptance -s lms --pdb --extra_args="lms/djangoapps/courseware/features/problems.feature" - -To run tests faster by not collecting static files or compiling sass, you can use -``paver test_acceptance -s lms --fasttest`` and -``paver test_acceptance -s cms --fasttest``. - -By default, all acceptance tests are run with the 'draft' ModuleStore. -To override the modulestore that is used, use the default\_store option. -Currently, the possible stores for acceptance tests are: 'split' -(xmodule.modulestore.split\_mongo.split\_draft.DraftVersioningModuleStore) -and 'draft' (xmodule.modulestore.mongo.DraftMongoModuleStore). For -example: paver test\_acceptance --default\_store='draft' Note, however, -all acceptance tests currently do not pass with 'split'. - -Acceptance tests will run on a randomized port and can be run in the -background of paver cms and lms or unit tests. To specify the port, -change the LETTUCE\_SERVER\_PORT constant in cms/envs/acceptance.py and -lms/envs/acceptance.py as well as the port listed in -cms/djangoapps/contentstore/feature/upload.py - -During acceptance test execution, Django log files are written to -``test_root/log/lms_acceptance.log`` and -``test_root/log/cms_acceptance.log``. - -**Note**: The acceptance tests can *not* currently run in parallel. - Running Tests on Paver Scripts ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -685,50 +617,6 @@ can find those in the following locations:: Do not commit the ``.po``, ``.mo``, ``.js`` files that are generated in the above locations during the dummy translation process! - -Debugging Acceptance Tests on Vagrant -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you are using a local Vagrant dev environment to run acceptance -tests, then you will only get console text output. To actually see what -is happening, you can turn on automatic screenshots. For each step two -screenshots will be taken - before, and after. To do this, simply add -the step:: - - Given I enable capturing of screenshots before and after each step - -to your scenario. This step can be added anywhere, and will enable -automatic screenshots for all following steps for that scenario only. -You can also use the step:: - - Given I disable capturing of screenshots before and after each step - -to turn off auto screenshots for all steps following it. - -Screenshots will be placed in the folder -``{TEST_ROOT}/log/auto_screenshots``. Each time you launch acceptance -tests, this folder will be cleaned. Each screenshot will be named -according to the template string -``{scenario_number}__{step_number}__{step_function_name}__{"1_before"|"2_after"}``. - -If you don't want to have screenshots be captured for all steps, but -rather want fine grained control, you can use this decorator before any Python function in ``feature_name.py`` file:: - - @capture_screenshot_before_after - -The decorator will capture two screenshots: one before the decorated function runs, -and one after. Also, this function is available, and can be inserted at any point in code to capture a -screenshot specifically in that place:: - - from lettuce import world; world.capture_screenshot("image_name") - -In both cases the captured screenshots will go to the same folder as when using the step method: ``{TEST_ROOT}/log/auto_screenshot``. - -A totally different approach to visually seeing acceptance tests run in -Vagrant is to redirect Vagrant X11 session to your local machine. Please -see https://github.com/edx/edx-platform/wiki/Test-engineering-FAQ for -instruction on how to achieve this. - Viewing Test Coverage --------------------- diff --git a/lms/djangoapps/courseware/features/__init__.py b/lms/djangoapps/courseware/features/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py deleted file mode 100644 index f07fa08d93113e02e9a60a440be738c761f47d4c..0000000000000000000000000000000000000000 --- a/lms/djangoapps/courseware/features/common.py +++ /dev/null @@ -1,240 +0,0 @@ -# pylint: disable=missing-docstring -# pylint: disable=no-member -# pylint: disable=redefined-outer-name - -from __future__ import absolute_import - -import time -from logging import getLogger - -from django.contrib.auth.models import User -from django.urls import reverse -from lettuce import step, world -from lettuce.django import django_url - -from courseware.courses import get_course_by_id -from student.models import CourseEnrollment -from xmodule import seq_module, vertical_block -from xmodule.course_module import CourseDescriptor -from xmodule.modulestore.django import modulestore - -logger = getLogger(__name__) - - -@step('I (.*) capturing of screenshots before and after each step$') -def configure_screenshots_for_all_steps(_step, action): - """ - A step to be used in *.feature files. Enables/disables - automatic saving of screenshots before and after each step in a - scenario. - """ - action = action.strip() - if action == 'enable': - world.auto_capture_screenshots = True - elif action == 'disable': - world.auto_capture_screenshots = False - else: - raise ValueError('Parameter `action` should be one of "enable" or "disable".') - - -@world.absorb -def capture_screenshot_before_after(func): - """ - A decorator that will take a screenshot before and after the applied - function is run. Use this if you do not want to capture screenshots - for each step in a scenario, but rather want to debug a single function. - """ - def inner(*args, **kwargs): - prefix = round(time.time() * 1000) - - world.capture_screenshot("{}_{}_{}".format( - prefix, func.func_name, 'before' - )) - ret_val = func(*args, **kwargs) - world.capture_screenshot("{}_{}_{}".format( - prefix, func.func_name, 'after' - )) - return ret_val - return inner - - -@step(u'The course "([^"]*)" exists$') -def create_course(_step, course): - - # First clear the modulestore so we don't try to recreate - # the same course twice - # This also ensures that the necessary templates are loaded - world.clear_courses() - - # Create the course - # We always use the same org and display name, - # but vary the course identifier (e.g. 600x or 191x) - world.scenario_dict['COURSE'] = world.CourseFactory.create( - org='edx', - number=course, - display_name='Test Course' - ) - - # Add a chapter to the course to contain problems - world.scenario_dict['CHAPTER'] = world.ItemFactory.create( - parent_location=world.scenario_dict['COURSE'].location, - category='chapter', - display_name='Test Chapter', - publish_item=True, # Not needed for direct-only but I'd rather the test didn't know that - ) - - world.scenario_dict['SECTION'] = world.ItemFactory.create( - parent_location=world.scenario_dict['CHAPTER'].location, - category='sequential', - display_name='Test Section', - publish_item=True, - ) - - -@step(u'I am registered for the course "([^"]*)"$') -def i_am_registered_for_the_course(step, course): - # Create the course - create_course(step, course) - - # Create the user - world.create_user('robot', 'test') - user = User.objects.get(username='robot') - - # If the user is not already enrolled, enroll the user. - # TODO: change to factory - CourseEnrollment.enroll(user, course_id(course)) - - world.log_in(username='robot', password='test') - - -@step(u'The course "([^"]*)" has extra tab "([^"]*)"$') -def add_tab_to_course(_step, course, extra_tab_name): - world.ItemFactory.create( - parent_location=course_location(course), - category="static_tab", - display_name=str(extra_tab_name)) - - -@step(u'I am in a course$') -def go_into_course(step): - step.given('I am registered for the course "6.002x"') - step.given('And I am logged in') - step.given('And I click on View Courseware') - - -# Do we really use these 3 w/ a different course than is in the scenario_dict? if so, why? If not, -# then get rid of the override arg -def course_id(course_num): - return world.scenario_dict['COURSE'].id.replace(course=course_num) - - -def course_location(course_num): - return world.scenario_dict['COURSE'].location.replace(course=course_num) - - -def section_location(course_num): - return world.scenario_dict['SECTION'].location.replace(course=course_num) - - -def visit_scenario_item(item_key): - """ - Go to the courseware page containing the item stored in `world.scenario_dict` - under the key `item_key` - """ - - url = django_url(reverse( - 'jump_to', - kwargs={ - 'course_id': unicode(world.scenario_dict['COURSE'].id), - 'location': unicode(world.scenario_dict[item_key].location), - } - )) - - world.browser.visit(url) - - -def get_courses(): - ''' - Returns dict of lists of courses available, keyed by course.org (ie university). - Courses are sorted by course.number. - ''' - courses = [c for c in modulestore().get_courses() - if isinstance(c, CourseDescriptor)] # skip error descriptors - courses = sorted(courses, key=lambda course: course.location.course) - return courses - - -def get_courseware_with_tabs(course_id): - """ - Given a course_id (string), return a courseware array of dictionaries for the - top three levels of navigation. Same as get_courseware() except include - the tabs on the right hand main navigation page. - - This hides the appropriate courseware as defined by the hide_from_toc field: - chapter.hide_from_toc - - Example: - - [{ - 'chapter_name': 'Overview', - 'sections': [{ - 'clickable_tab_count': 0, - 'section_name': 'Welcome', - 'tab_classes': [] - }, { - 'clickable_tab_count': 1, - 'section_name': 'System Usage Sequence', - 'tab_classes': ['VerticalBlock'] - }, { - 'clickable_tab_count': 0, - 'section_name': 'Lab0: Using the tools', - 'tab_classes': ['HtmlDescriptor', 'HtmlDescriptor', 'CapaDescriptor'] - }, { - 'clickable_tab_count': 0, - 'section_name': 'Circuit Sandbox', - 'tab_classes': [] - }] - }, { - 'chapter_name': 'Week 1', - 'sections': [{ - 'clickable_tab_count': 4, - 'section_name': 'Administrivia and Circuit Elements', - 'tab_classes': ['VerticalBlock', 'VerticalBlock', 'VerticalBlock', 'VerticalBlock'] - }, { - 'clickable_tab_count': 0, - 'section_name': 'Basic Circuit Analysis', - 'tab_classes': ['CapaDescriptor', 'CapaDescriptor', 'CapaDescriptor'] - }, { - 'clickable_tab_count': 0, - 'section_name': 'Resistor Divider', - 'tab_classes': [] - }, { - 'clickable_tab_count': 0, - 'section_name': 'Week 1 Tutorials', - 'tab_classes': [] - }] - }, { - 'chapter_name': 'Midterm Exam', - 'sections': [{ - 'clickable_tab_count': 2, - 'section_name': 'Midterm Exam', - 'tab_classes': ['VerticalBlock', 'VerticalBlock'] - }] - }] - """ - - course = get_course_by_id(course_id) - chapters = [chapter for chapter in course.get_children() if not chapter.hide_from_toc] - courseware = [{ - 'chapter_name': c.display_name_with_default_escaped, - 'sections': [{ - 'section_name': s.display_name_with_default_escaped, - 'clickable_tab_count': len(s.get_children()) if isinstance(s, seq_module.SequenceDescriptor) else 0, - 'tabs': [{ - 'children_count': len(t.get_children()) if isinstance(t, vertical_block.VerticalBlock) else 0, - 'class': t.__class__.__name__} for t in s.get_children() - ] - } for s in c.get_children() if not s.hide_from_toc] - } for c in chapters] - - return courseware diff --git a/lms/djangoapps/courseware/features/courseware.py b/lms/djangoapps/courseware/features/courseware.py deleted file mode 100644 index db19fd1970832b1b4b7ce7bdb5b5b3d4adda7821..0000000000000000000000000000000000000000 --- a/lms/djangoapps/courseware/features/courseware.py +++ /dev/null @@ -1,11 +0,0 @@ -# pylint: disable=missing-docstring -# pylint: disable=redefined-outer-name - -from lettuce import step, world -from lettuce.django import django_url - - -@step('I visit the courseware URL$') -def i_visit_the_course_info_url(step): - url = django_url('/courses/MITx/6.002x/2012_Fall/courseware') - world.browser.visit(url) diff --git a/lms/djangoapps/courseware/features/courseware_common.py b/lms/djangoapps/courseware/features/courseware_common.py deleted file mode 100644 index ba2f2193a775b56edf356ff406499f7d479733ed..0000000000000000000000000000000000000000 --- a/lms/djangoapps/courseware/features/courseware_common.py +++ /dev/null @@ -1,46 +0,0 @@ -# pylint: disable=missing-docstring -# pylint: disable=redefined-outer-name -# pylint: disable=unused-argument - -from lettuce import step, world - - -@step('I click on View Courseware') -def i_click_on_view_courseware(step): - world.css_click('a.enter-course') - - -@step('I click on the "([^"]*)" tab$') -def i_click_on_the_tab(step, tab_text): - world.click_link(tab_text) - - -@step('I click the "([^"]*)" button$') -def i_click_on_the_button(step, data_attr): - world.click_button(data_attr) - - -@step('I click on the "([^"]*)" link$') -def i_click_on_the_link(step, link_text): - world.click_link(link_text) - - -@step('I visit the courseware URL$') -def i_visit_the_course_info_url(step): - world.visit('/courses/MITx/6.002x/2012_Fall/courseware') - - -@step(u'I am on the dashboard page$') -def i_am_on_the_dashboard_page(step): - assert world.is_css_present('section.courses') - assert world.url_equals('/dashboard') - - -@step('the "([^"]*)" tab is active$') -def the_tab_is_active(step, tab_text): - assert world.css_text('.course-tabs a.active') == tab_text - - -@step('the login dialog is visible$') -def login_dialog_visible(step): - assert world.css_visible('form#login_form.login_form') diff --git a/lms/djangoapps/courseware/features/events.py b/lms/djangoapps/courseware/features/events.py deleted file mode 100644 index 3133186fce3bd5ac52ceff009b72b2d311064c77..0000000000000000000000000000000000000000 --- a/lms/djangoapps/courseware/features/events.py +++ /dev/null @@ -1,82 +0,0 @@ -# pylint: disable=missing-docstring - -from django.conf import settings -from lettuce import before, step, world -from pymongo import MongoClient - -from openedx.core.lib.tests.tools import assert_equals, assert_in # pylint: disable=no-name-in-module - -REQUIRED_EVENT_FIELDS = [ - 'agent', - 'event', - 'event_source', - 'event_type', - 'host', - 'ip', - 'page', - 'time', - 'username' -] - - -@before.all # pylint: disable=no-member -def connect_to_mongodb(): - world.mongo_client = MongoClient(host=settings.MONGO_HOST, port=settings.MONGO_PORT_NUM) - world.event_collection = world.mongo_client['track']['events'] - - -@before.each_scenario # pylint: disable=no-member -def reset_captured_events(_scenario): - world.event_collection.drop() - - -@before.outline # pylint: disable=no-member -def reset_between_outline_scenarios(_scenario, _order, _outline, _reasons_to_fail): - world.event_collection.drop() - - -@step(r'[aA]n? course url "(.*)" event is emitted$') -def course_url_event_is_emitted(_step, url_regex): - event_type = url_regex.format(world.scenario_dict['COURSE'].id) # pylint: disable=no-member - n_events_are_emitted(_step, 1, event_type, "server") - - -@step(r'([aA]n?|\d+) "(.*)" (server|browser) events? is emitted$') -def n_events_are_emitted(_step, count, event_type, event_source): - - # Ensure all events are written out to mongo before querying. - world.mongo_client.fsync() - - # Note that splinter makes 2 requests when you call browser.visit('/foo') - # the first just checks to see if the server responds with a status - # code of 200, the next actually uses the browser to submit the request. - # We filter out events associated with the status code checks by ignoring - # events that come directly from splinter. - criteria = { - 'event_type': event_type, - 'event_source': event_source, - 'agent': { - '$ne': 'python/splinter' - } - } - - cursor = world.event_collection.find(criteria) - - try: - number_events = int(count) - except ValueError: - number_events = 1 - - assert_equals(cursor.count(), number_events) - - event = cursor.next() - - expected_field_values = { - "username": world.scenario_dict['USER'].username, # pylint: disable=no-member - "event_type": event_type, - } - for key, value in expected_field_values.iteritems(): - assert_equals(event[key], value) - - for field in REQUIRED_EVENT_FIELDS: - assert_in(field, event) diff --git a/lms/djangoapps/courseware/features/lti.feature b/lms/djangoapps/courseware/features/lti.feature deleted file mode 100644 index 39d9b9bd6df0b34b2f448a5df1e947e61b37b18f..0000000000000000000000000000000000000000 --- a/lms/djangoapps/courseware/features/lti.feature +++ /dev/null @@ -1,150 +0,0 @@ -@shard_1 @requires_stub_lti -Feature: LMS.LTI component - As a student, I want to view LTI component in LMS. - - #1 - Scenario: LTI component in LMS with no launch_url is not rendered - Given the course has correct LTI credentials with registered Instructor - And the course has an LTI component with no_launch_url fields: - | open_in_a_new_page | - | False | - Then I view the LTI and error is shown - - #2 - Scenario: LTI component in LMS with incorrect lti_id is rendered incorrectly - Given the course has correct LTI credentials with registered Instructor - And the course has an LTI component with incorrect_lti_id fields: - | open_in_a_new_page | - | False | - Then I view the LTI but incorrect_signature warning is rendered - - #3 - Scenario: LTI component in LMS is rendered incorrectly - Given the course has incorrect LTI credentials - And the course has an LTI component with correct fields: - | open_in_a_new_page | - | False | - Then I view the LTI but incorrect_signature warning is rendered - - #5 - Scenario: LTI component in LMS is correctly rendered in iframe - Given the course has correct LTI credentials with registered Instructor - And the course has an LTI component with correct fields: - | open_in_a_new_page | - | False | - Then I view the LTI and it is rendered in iframe - - #6 - Scenario: Graded LTI component in LMS is correctly works - Given the course has correct LTI credentials with registered Instructor - And the course has an LTI component with correct fields: - | open_in_a_new_page | weight | graded | has_score | - | False | 10 | True | True | - And I submit answer to LTI 1 question - And I click on the "Progress" tab - Then I see text "Problem Scores: 5/10" - And I see graph with total progress "5%" - Then I click on the "Instructor" tab - And I click the "Student Admin" button - And I click on the "View Gradebook" link - And I see in the gradebook table that "HW" is "50" - And I see in the gradebook table that "Total" is "5" - - #7 - Scenario: Graded LTI component in LMS role's masquerading correctly works - Given the course has correct LTI credentials with registered Instructor - And the course has an LTI component with correct fields: - | open_in_a_new_page | has_score | - | False | True | - And I view the LTI and it is rendered in iframe - And I see in iframe that LTI role is Instructor - And I switch to student - And I view the LTI and it is rendered in iframe - Then I see in iframe that LTI role is Student - - #8 - Scenario: Graded LTI component in LMS is correctly works with beta testers - Given the course has correct LTI credentials with registered BetaTester - And the course has an LTI component with correct fields: - | open_in_a_new_page | weight | graded | has_score | - | False | 10 | True | True | - And I submit answer to LTI 1 question - And I click on the "Progress" tab - Then I see text "Problem Scores: 5/10" - And I see graph with total progress "5%" - - #9 - Scenario: Graded LTI component in LMS is correctly works with LTI2v0 PUT callback - Given the course has correct LTI credentials with registered Instructor - And the course has an LTI component with correct fields: - | open_in_a_new_page | weight | graded | has_score | - | False | 10 | True | True | - And I submit answer to LTI 2 question - And I click on the "Progress" tab - Then I see text "Problem Scores: 8/10" - And I see graph with total progress "8%" - Then I click on the "Instructor" tab - And I click the "Student Admin" button - And I click on the "View Gradebook" link - And I see in the gradebook table that "HW" is "80" - And I see in the gradebook table that "Total" is "8" - And I visit the LTI component - Then I see LTI component progress with text "(8.0 / 10.0 points)" - Then I see LTI component feedback with text "This is awesome." - - #10 - Scenario: Graded LTI component in LMS is correctly works with LTI2v0 PUT delete callback - Given the course has correct LTI credentials with registered Instructor - And the course has an LTI component with correct fields: - | open_in_a_new_page | weight | graded | has_score | - | False | 10 | True | True | - And I submit answer to LTI 2 question - And I visit the LTI component - Then I see LTI component progress with text "(8.0 / 10.0 points)" - Then I see LTI component feedback with text "This is awesome." - And the LTI provider deletes my grade and feedback - And I visit the LTI component (have to reload) - Then I see LTI component progress with text "(10.0 points possible)" - Then in the LTI component I do not see feedback - And I click on the "Progress" tab - Then I see text "Problem Scores: 0/10" - And I see graph with total progress "0%" - Then I click on the "Instructor" tab - And I click the "Student Admin" button - And I click on the "View Gradebook" link - And I see in the gradebook table that "HW" is "0" - And I see in the gradebook table that "Total" is "0" - - #11 - Scenario: LTI component that set to hide_launch and open_in_a_new_page shows no button - Given the course has correct LTI credentials with registered Instructor - And the course has an LTI component with correct fields: - | open_in_a_new_page | hide_launch | - | False | True | - Then in the LTI component I do not see a launch button - Then I see LTI component module title with text "LTI (External resource)" - - #12 - Scenario: LTI component that set to hide_launch and not open_in_a_new_page shows no iframe - Given the course has correct LTI credentials with registered Instructor - And the course has an LTI component with correct fields: - | open_in_a_new_page | hide_launch | - | True | True | - Then in the LTI component I do not see an provider iframe - Then I see LTI component module title with text "LTI (External resource)" - - #13 - Scenario: LTI component button text is correctly displayed - Given the course has correct LTI credentials with registered Instructor - And the course has an LTI component with correct fields: - | button_text | - | Launch Application | - Then I see LTI component button with text "Launch Application" - - #14 - Scenario: LTI component description is correctly displayed - Given the course has correct LTI credentials with registered Instructor - And the course has an LTI component with correct fields: - | description | - | Application description | - Then I see LTI component description with text "Application description" diff --git a/lms/djangoapps/courseware/features/lti.py b/lms/djangoapps/courseware/features/lti.py deleted file mode 100644 index a0e273802ad91660827edc2297a0093861d8b9f2..0000000000000000000000000000000000000000 --- a/lms/djangoapps/courseware/features/lti.py +++ /dev/null @@ -1,345 +0,0 @@ -# pylint: disable=missing-docstring -# pylint: disable=no-member -import datetime -import os - -import pytz -from django.conf import settings -from lettuce import step, world -from mock import patch -from pytz import UTC -from splinter.exceptions import ElementDoesNotExist - -from common import visit_scenario_item -from courseware.access import has_access -from courseware.tests.factories import BetaTesterFactory, InstructorFactory -from openedx.core.lib.tests.tools import assert_equal, assert_in, assert_true # pylint: disable=no-name-in-module -from student.tests.factories import UserFactory - -TEST_COURSE_NAME = "test_course_a" - - -@step('I view the LTI and error is shown$') -def lti_is_not_rendered(_step): - # error is shown - assert world.is_css_present('.error_message', wait_time=0) - - # iframe is not presented - assert not world.is_css_present('iframe', wait_time=0) - - # link is not presented - assert not world.is_css_present('.link_lti_new_window', wait_time=0) - - -def check_lti_iframe_content(text): - # inside iframe test content is presented - location = world.scenario_dict['LTI'].location.html_id() - iframe_name = 'ltiFrame-' + location - with world.browser.get_iframe(iframe_name) as iframe: - # iframe does not contain functions from terrain/ui_helpers.py - assert iframe.is_element_present_by_css('.result', wait_time=0) - assert (text == world.retry_on_exception( - lambda: iframe.find_by_css('.result')[0].text, - max_attempts=5 - )) - - -@step('I view the LTI and it is rendered in iframe$') -def lti_is_rendered_iframe(_step): - world.wait_for_present('iframe') # pylint: disable=no-member - assert world.is_css_present('iframe', wait_time=2) # pylint: disable=no-member - assert not world.is_css_present('.link_lti_new_window', wait_time=0) # pylint: disable=no-member - assert not world.is_css_present('.error_message', wait_time=0) # pylint: disable=no-member - - # iframe is visible - assert world.css_visible('iframe') # pylint: disable=no-member - check_lti_iframe_content("This is LTI tool. Success.") - - -@step('I view the LTI but incorrect_signature warning is rendered$') -def incorrect_lti_is_rendered(_step): - assert world.is_css_present('iframe', wait_time=2) - assert not world.is_css_present('.link_lti_new_window', wait_time=0) - assert not world.is_css_present('.error_message', wait_time=0) - - # inside iframe test content is presented - check_lti_iframe_content("Wrong LTI signature") - - -@step('the course has correct LTI credentials with registered (.*)$') -def set_correct_lti_passport(_step, user='Instructor'): - coursenum = TEST_COURSE_NAME - metadata = { - 'lti_passports': ["correct_lti_id:test_client_key:test_client_secret"] - } - - i_am_registered_for_the_course(coursenum, metadata, user) - - -@step('the course has incorrect LTI credentials$') -def set_incorrect_lti_passport(_step): - coursenum = TEST_COURSE_NAME - metadata = { - 'lti_passports': ["test_lti_id:test_client_key:incorrect_lti_secret_key"] - } - - i_am_registered_for_the_course(coursenum, metadata) - - -@step(r'the course has an LTI component with (.*) fields(?:\:)?$') # , new_page is(.*), graded is(.*) -def add_correct_lti_to_course(_step, fields): - category = 'lti' - host = getattr(settings, 'LETTUCE_HOST', '127.0.0.1') - metadata = { - 'lti_id': 'correct_lti_id', - 'launch_url': 'http://{}:{}/correct_lti_endpoint'.format(host, settings.LTI_PORT), - } - - if fields.strip() == 'incorrect_lti_id': # incorrect fields - metadata.update({ - 'lti_id': 'incorrect_lti_id' - }) - elif fields.strip() == 'correct': # correct fields - pass - elif fields.strip() == 'no_launch_url': - metadata.update({ - 'launch_url': u'' - }) - else: # incorrect parameter - assert False - - if _step.hashes: - metadata.update(_step.hashes[0]) - - world.scenario_dict['LTI'] = world.ItemFactory.create( - parent_location=world.scenario_dict['SECTION'].location, - category=category, - display_name='LTI', - metadata=metadata, - ) - - visit_scenario_item('LTI') - - -def create_course_for_lti(course, metadata): - # First clear the modulestore so we don't try to recreate - # the same course twice - # This also ensures that the necessary templates are loaded - world.clear_courses() - - weight = 0.1 - grading_policy = { - "GRADER": [ - { - "type": "Homework", - "min_count": 1, - "drop_count": 0, - "short_label": "HW", - "weight": weight - }, - ] - } - - # Create the course - # We always use the same org and display name, - # but vary the course identifier (e.g. 600x or 191x) - world.scenario_dict['COURSE'] = world.CourseFactory.create( - org='edx', - number=course, - display_name='Test Course', - metadata=metadata, - grading_policy=grading_policy, - ) - - # Add a section to the course to contain problems - world.scenario_dict['CHAPTER'] = world.ItemFactory.create( - parent_location=world.scenario_dict['COURSE'].location, - category='chapter', - display_name='Test Chapter', - ) - world.scenario_dict['SECTION'] = world.ItemFactory.create( - parent_location=world.scenario_dict['CHAPTER'].location, - category='sequential', - display_name='Test Section', - metadata={'graded': True, 'format': 'Homework'}) - - -@patch.dict('courseware.access.settings.FEATURES', {'DISABLE_START_DATES': False}) -def i_am_registered_for_the_course(coursenum, metadata, user='Instructor'): - # Create user - if user == 'BetaTester': - # Create the course - now = datetime.datetime.now(pytz.UTC) - tomorrow = now + datetime.timedelta(days=5) - metadata.update({'days_early_for_beta': 5, 'start': tomorrow}) - create_course_for_lti(coursenum, metadata) - course_descriptor = world.scenario_dict['COURSE'] - - # create beta tester - user = BetaTesterFactory(course_key=course_descriptor.id) - normal_student = UserFactory() - instructor = InstructorFactory(course_key=course_descriptor.id) - - assert not has_access(normal_student, 'load', course_descriptor) - assert has_access(user, 'load', course_descriptor) - assert has_access(instructor, 'load', course_descriptor) - else: - metadata.update({'start': datetime.datetime(1970, 1, 1, tzinfo=UTC)}) - create_course_for_lti(coursenum, metadata) - course_descriptor = world.scenario_dict['COURSE'] - user = InstructorFactory(course_key=course_descriptor.id) - - # Enroll the user in the course and log them in - if has_access(user, 'load', course_descriptor): - world.enroll_user(user, course_descriptor.id) - - world.log_in(username=user.username, password='test') - - -def check_lti_popup(parent_window): - # You should now have 2 browser windows open, the original courseware and the LTI - windows = world.browser.windows - assert_equal(len(windows), 2) - - # For verification, iterate through the window titles and make sure that - # both are there. - tabs = [] - expected_tabs = [ - u'LTI | Test Section | {course} Courseware | {platform}'.format( - course=TEST_COURSE_NAME, - platform=settings.PLATFORM_NAME - ), - u'TEST TITLE' - ] - - for window in windows: - world.browser.switch_to_window(window) - tabs.append(world.browser.title) - assert_equal(tabs, expected_tabs) - - # Now verify the contents of the LTI window (which is the 2nd window/tab) - # Note: The LTI opens in a new browser window, but Selenium sticks with the - # current window until you explicitly switch to the context of the new one. - world.browser.switch_to_window(windows[1]) - url = world.browser.url - basename = os.path.basename(url) - pathname = os.path.splitext(basename)[0] - assert_equal(pathname, u'correct_lti_endpoint') - - result = world.css_find('.result').first.text - assert_equal(result, u'This is LTI tool. Success.') - - world.browser.driver.close() # Close the pop-up window - world.browser.switch_to_window(parent_window) # Switch to the main window again - - -def click_and_check_lti_popup(): - parent_window = world.browser.current_window # Save the parent window - world.css_find('.link_lti_new_window').first.click() - check_lti_popup(parent_window) - - -@step('visit the LTI component') -def visit_lti_component(_step): - visit_scenario_item('LTI') - - -@step('I see LTI component (.*) with text "([^"]*)"$') -def see_elem_text(_step, elem, text): - selector_map = { - 'progress': '.problem-progress', - 'feedback': '.problem-feedback', - 'module title': '.problem-header', - 'button': '.link_lti_new_window', - 'description': '.lti-description' - } - assert_in(elem, selector_map) - assert_true(world.css_has_text(selector_map[elem], text)) - - -@step('I see text "([^"]*)"$') -def check_progress(_step, text): - assert world.browser.is_text_present(text) - - -@step('I see graph with total progress "([^"]*)"$') -def see_graph(_step, progress): - assert_equal(progress, world.css_find('#grade-detail-graph .overallGrade').first.text.split('\n')[1]) - - -@step('I see in the gradebook table that "([^"]*)" is "([^"]*)"$') -def see_value_in_the_gradebook(_step, label, text): - table_selector = '.grade-table' - index = 0 - table_headers = world.css_find(u'{0} thead th'.format(table_selector)) - - for i, element in enumerate(table_headers): - if element.text.strip() == label: - index = i - break - - assert_true(world.css_has_text(u'{0} tbody td'.format(table_selector), text, index=index)) - - -@step('I submit answer to LTI (.*) question$') -def click_grade(_step, version): - version_map = { - '1': {'selector': 'submit-button', 'expected_text': 'LTI consumer (edX) responded with XML content'}, - '2': {'selector': 'submit-lti2-button', 'expected_text': 'LTI consumer (edX) responded with HTTP 200'}, - } - assert_in(version, version_map) - location = world.scenario_dict['LTI'].location.html_id() - iframe_name = 'ltiFrame-' + location - with world.browser.get_iframe(iframe_name) as iframe: - css_ele = version_map[version]['selector'] - css_loc = '#' + css_ele - world.wait_for_visible(css_loc) - world.css_click(css_loc) - assert iframe.is_text_present(version_map[version]['expected_text']) - - -@step('LTI provider deletes my grade and feedback$') -def click_delete_button(_step): - with world.browser.get_iframe(get_lti_frame_name()) as iframe: - iframe.find_by_name('submit-lti2-delete-button').first.click() - - -def get_lti_frame_name(): - location = world.scenario_dict['LTI'].location.html_id() - return 'ltiFrame-' + location - - -@step('I see in iframe that LTI role is (.*)$') -def check_role(_step, role): - world.wait_for_present('iframe') - location = world.scenario_dict['LTI'].location.html_id() - iframe_name = 'ltiFrame-' + location - with world.browser.get_iframe(iframe_name) as iframe: - expected_role = 'Role: ' + role - role = world.retry_on_exception( - lambda: iframe.find_by_tag('h5').first.value, - max_attempts=5, - ignored_exceptions=ElementDoesNotExist - ) - assert_equal(expected_role, role) - - -@step('I switch to (.*)$') -def switch_view(_step, view): - staff_status = world.css_find('#action-preview-select').first.value - if staff_status != view: - world.browser.select("select", view) - world.wait_for_ajax_complete() - assert_equal(world.css_find('#action-preview-select').first.value, view) - - -@step("in the LTI component I do not see (.*)$") -def check_lti_component_no_elem(_step, text): - selector_map = { - 'a launch button': '.link_lti_new_window', - 'an provider iframe': '.ltiLaunchFrame', - 'feedback': '.problem-feedback', - 'progress': '.problem-progress', - } - assert_in(text, selector_map) - assert_true(world.is_css_not_present(selector_map[text])) diff --git a/lms/djangoapps/courseware/features/problems_setup.py b/lms/djangoapps/courseware/features/problems_setup.py deleted file mode 100644 index 1713ef0fb1bb6e100203f2e2a82475dc469b3220..0000000000000000000000000000000000000000 --- a/lms/djangoapps/courseware/features/problems_setup.py +++ /dev/null @@ -1,460 +0,0 @@ -# pylint: disable=missing-docstring - -# EVERY PROBLEM TYPE MUST HAVE THE FOLLOWING: -# -Section in Dictionary containing: -# -factory -# -kwargs -# -(optional metadata) -# -Correct, Incorrect and Unanswered CSS selectors -# -A way to answer the problem correctly and incorrectly -# -A way to check the problem was answered correctly, incorrectly and blank - -import random -import textwrap - -from lettuce import world - -from capa.tests.response_xml_factory import ( - ChoiceResponseXMLFactory, - ChoiceTextResponseXMLFactory, - CodeResponseXMLFactory, - CustomResponseXMLFactory, - FormulaResponseXMLFactory, - ImageResponseXMLFactory, - MultipleChoiceResponseXMLFactory, - NumericalResponseXMLFactory, - OptionResponseXMLFactory, - StringResponseXMLFactory -) -from common import section_location - -# Factories from capa.tests.response_xml_factory that we will use -# to generate the problem XML, with the keyword args used to configure -# the output. -# 'correct', 'incorrect', and 'unanswered' keys are lists of CSS selectors -# the presence of any in the list is sufficient -PROBLEM_DICT = { - 'drop down': { - 'factory': OptionResponseXMLFactory(), - 'kwargs': { - 'question_text': 'The correct answer is Option 2', - 'options': ['Option 1', 'Option 2', 'Option 3', 'Option 4'], - 'correct_option': 'Option 2'}, - 'correct': ['span.correct'], - 'incorrect': ['span.incorrect'], - 'unanswered': ['span.unanswered']}, - - 'multiple choice': { - 'factory': MultipleChoiceResponseXMLFactory(), - 'kwargs': { - 'question_text': 'The correct answer is Choice 3', - 'choices': [False, False, True, False], - 'choice_names': ['choice_0', 'choice_1', 'choice_2', 'choice_3']}, - 'correct': ['label.choicegroup_correct', 'span.correct'], - 'incorrect': ['label.choicegroup_incorrect', 'span.incorrect'], - 'unanswered': ['span.unanswered']}, - - 'checkbox': { - 'factory': ChoiceResponseXMLFactory(), - 'kwargs': { - 'question_text': 'The correct answer is Choices 1 and 3', - 'choice_type': 'checkbox', - 'choices': [True, False, True, False, False], - 'choice_names': ['Choice 1', 'Choice 2', 'Choice 3', 'Choice 4']}, - 'correct': ['span.correct'], - 'incorrect': ['span.incorrect'], - 'unanswered': ['span.unanswered']}, - - 'radio': { - 'factory': ChoiceResponseXMLFactory(), - 'kwargs': { - 'question_text': 'The correct answer is Choice 3', - 'choice_type': 'radio', - 'choices': [False, False, True, False], - 'choice_names': ['Choice 1', 'Choice 2', 'Choice 3', 'Choice 4']}, - 'correct': ['label.choicegroup_correct', 'span.correct'], - 'incorrect': ['label.choicegroup_incorrect', 'span.incorrect'], - 'unanswered': ['span.unanswered']}, - - 'string': { - 'factory': StringResponseXMLFactory(), - 'kwargs': { - 'question_text': 'The answer is "correct string"', - 'case_sensitive': False, - 'answer': 'correct string'}, - 'correct': ['div.correct'], - 'incorrect': ['div.incorrect'], - 'unanswered': ['div.unanswered', 'div.unsubmitted']}, - - 'numerical': { - 'factory': NumericalResponseXMLFactory(), - 'kwargs': { - 'question_text': 'The answer is pi + 1', - 'answer': '4.14159', - 'tolerance': '0.00001', - 'math_display': True}, - 'correct': ['div.correct'], - 'incorrect': ['div.incorrect'], - 'unanswered': ['div.unanswered', 'div.unsubmitted']}, - - 'formula': { - 'factory': FormulaResponseXMLFactory(), - 'kwargs': { - 'question_text': 'The solution is [mathjax]x^2+2x+y[/mathjax]', - 'sample_dict': {'x': (-100, 100), 'y': (-100, 100)}, - 'num_samples': 10, - 'tolerance': 0.00001, - 'math_display': True, - 'answer': 'x^2+2*x+y'}, - 'correct': ['div.correct'], - 'incorrect': ['div.incorrect'], - 'unanswered': ['div.unanswered', 'div.unsubmitted']}, - - 'script': { - 'factory': CustomResponseXMLFactory(), - 'kwargs': { - 'question_text': 'Enter two integers that sum to 10.', - 'cfn': 'test_add_to_ten', - 'expect': '10', - 'num_inputs': 2, - 'script': textwrap.dedent(""" - def test_add_to_ten(expect,ans): - try: - a1=int(ans[0]) - a2=int(ans[1]) - except ValueError: - a1=0 - a2=0 - return (a1+a2)==int(expect) - """)}, - 'correct': ['div.correct'], - 'incorrect': ['div.incorrect'], - 'unanswered': ['div.unanswered', 'div.unsubmitted']}, - - 'code': { - 'factory': CodeResponseXMLFactory(), - 'kwargs': { - 'question_text': 'Submit code to an external grader', - 'initial_display': 'print "Hello world!"', - 'grader_payload': '{"grader": "ps1/Spring2013/test_grader.py"}', }, - 'correct': ['span.correct'], - 'incorrect': ['span.incorrect'], - 'unanswered': ['span.unanswered']}, - - 'radio_text': { - 'factory': ChoiceTextResponseXMLFactory(), - 'kwargs': { - 'question_text': 'The correct answer is Choice 0 and input 8', - 'type': 'radiotextgroup', - 'choices': [("true", {"answer": "8", "tolerance": "1"}), - ("false", {"answer": "8", "tolerance": "1"}) - ] - }, - 'correct': ['section.choicetextgroup_correct'], - 'incorrect': ['section.choicetextgroup_incorrect', 'span.incorrect'], - 'unanswered': ['span.unanswered']}, - - 'checkbox_text': { - 'factory': ChoiceTextResponseXMLFactory(), - 'kwargs': { - 'question_text': 'The correct answer is Choice 0 and input 8', - 'type': 'checkboxtextgroup', - 'choices': [("true", {"answer": "8", "tolerance": "1"}), - ("false", {"answer": "8", "tolerance": "1"}) - ] - }, - 'correct': ['span.correct'], - 'incorrect': ['span.incorrect'], - 'unanswered': ['span.unanswered']}, - - 'image': { - 'factory': ImageResponseXMLFactory(), - 'kwargs': { - 'src': '/static/images/placeholder-image.png', - 'rectangle': '(50,50)-(100,100)' - }, - 'correct': ['span.correct'], - 'incorrect': ['span.incorrect'], - 'unanswered': ['span.unanswered']} -} - - -def answer_problem(course, problem_type, correctness): - # Make sure that the problem has been completely rendered before - # starting to input an answer. - world.wait_for_ajax_complete() - - section_loc = section_location(course) - - if problem_type == "drop down": - select_name = "input_{}_2_1".format( - section_loc.course_key.make_usage_key('problem', 'drop_down').html_id() - ) - option_text = 'Option 2' if correctness == 'correct' else 'Option 3' - world.select_option(select_name, option_text) - - elif problem_type == "multiple choice": - if correctness == 'correct': - world.css_check(inputfield(course, 'multiple choice', choice='choice_2')) - else: - world.css_check(inputfield(course, 'multiple choice', choice='choice_1')) - - elif problem_type == "checkbox": - if correctness == 'correct': - world.css_check(inputfield(course, 'checkbox', choice='choice_0')) - world.css_check(inputfield(course, 'checkbox', choice='choice_2')) - else: - world.css_check(inputfield(course, 'checkbox', choice='choice_3')) - - elif problem_type == 'radio': - if correctness == 'correct': - world.css_check(inputfield(course, 'radio', choice='choice_2')) - else: - world.css_check(inputfield(course, 'radio', choice='choice_1')) - - elif problem_type == 'string': - textvalue = 'correct string' if correctness == 'correct' else 'incorrect' - world.css_fill(inputfield(course, 'string'), textvalue) - - elif problem_type == 'numerical': - textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2, 2)) - world.css_fill(inputfield(course, 'numerical'), textvalue) - - elif problem_type == 'formula': - textvalue = "x^2+2*x+y" if correctness == 'correct' else 'x^2' - world.css_fill(inputfield(course, 'formula'), textvalue) - - elif problem_type == 'script': - # Correct answer is any two integers that sum to 10 - first_addend = random.randint(-100, 100) - second_addend = 10 - first_addend - - # If we want an incorrect answer, then change - # the second addend so they no longer sum to 10 - if correctness == 'incorrect': - second_addend += random.randint(1, 10) - - world.css_fill(inputfield(course, 'script', input_num=1), str(first_addend)) - world.css_fill(inputfield(course, 'script', input_num=2), str(second_addend)) - - elif problem_type == 'code': - # The fake xqueue server is configured to respond - # correct / incorrect no matter what we submit. - # Furthermore, since the inline code response uses - # JavaScript to make the code display nicely, it's difficult - # to programatically input text - # (there's not <textarea> 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 87e557f19c8e5bcb7d4cd66983bdb4d1cc6c2e95..0000000000000000000000000000000000000000 --- 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 10acbf9447356e4458f82fd5f7695614223b20c7..3441805245269d1caa0669acacd37820e9b71920 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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/lms/djangoapps/instructor/features/common.py b/lms/djangoapps/instructor/features/common.py deleted file mode 100644 index f191ba1a4695a0e621ff065fab47ac1ccdb22c0d..0000000000000000000000000000000000000000 --- 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 3de43355b71c76ce4cdf0ed7658019384a375e89..0000000000000000000000000000000000000000 --- 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: -<class 'xmodule.graders.WeightedSubsectionsGrader'> - -Graded sections: - subgrader=<class 'xmodule.graders.AssignmentFormatGrader'>, type=Homework, category=Homework, weight=0.15 - subgrader=<class 'xmodule.graders.AssignmentFormatGrader'>, type=Lab, category=Lab, weight=0.15 - subgrader=<class 'xmodule.graders.AssignmentFormatGrader'>, type=Midterm Exam, category=Midterm Exam, weight=0.3 - subgrader=<class 'xmodule.graders.AssignmentFormatGrader'>, 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 15f050f955861f92fa7a893aa2466d332d7e872c..0000000000000000000000000000000000000000 --- 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 6a9aa2da684b5a866c6be93c8f9dbc06654e7f38..0000000000000000000000000000000000000000 --- 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 7960c91ad3c0c9f8a06b5619d4b8accb36e1e45e..80754cf12712edd51fbf310a7878fb4a1a5f8a13 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 8ec7376c01e638d79c63c610cdc4eecb34110e6d..0000000000000000000000000000000000000000 --- 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 9d5f4845eeb596f0a44c30706ec0f91bf1443718..ee695d55936da5845442b6ad8b2fb1af0a689637 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 afa24c56c93975bdd1f92155799b1467b2049d15..0000000000000000000000000000000000000000 --- 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 8e7a346403f5df8dedfd6cc96ca272a53c0c2d2e..8746d869920bbb7ed2074152c2fef2f97f41ef78 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 70950d7ae05f836261d276f68c3fbee3144adb8c..9a7bcace73022430b3346f2142aa94ca0a6dfd72 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 f1029dbec6f9e29d2aecfa36da8c48848b01ef62..0000000000000000000000000000000000000000 --- 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 572b7520235493487e67f6588aa9ec09eefb3c89..8338fbe8bcbc467975551e0b86603fe703b747e2 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 9df898f572649b2346a18298caee3da9bab85711..6d741701ab488e26c19c5fb7e15a32eef354fe1a 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 78ae2d892dcca46bdd55836048a84fc7bd6ebb83..5baa9b5aedb9a7fa3c60b0fd273d48c9ad8890f4 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 45aecbb6f77db46dc4dfeb730d4107a492dbf153..e29993274292fbaf72833f36076fd8d2df1b7764 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 3b33268bc3c0b378bf2ada61973bd493fe042e5c..336c9e61bb2f6603f8d71ed6abb0f5e8542aae1d 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 a61f7f934b5f05a9a1f5d6589285643177c821bc..0000000000000000000000000000000000000000 --- 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 8d98bc2822d39b0523a019950f56af0950cfaacd..dfd4409d928ec911e020b4f62a29cfa24da5f358 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 5077b338872ccfc7980e5758ad0833d86a03fb7b..90a9dc17b2dbe6b8f8082b9df2bb40aeb4bdf9e2 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"