diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 8a34a2faea..58980fe06e 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -283,24 +283,33 @@ def button_disabled(step, 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 cms/static/js/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 = 'li.nav-item > a.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): - - def click_button(btn_css): - world.css_click(btn_css) - return world.css_find(btn_css).visible == False - - prompt_css = 'div.prompt.has-actions' - world.wait_for_visible(prompt_css) - - btn_css = 'a.button.action-primary' - world.wait_for_visible(btn_css) - - # Sometimes you can do a click before the prompt is up. - # Thus we need some retry logic here. - world.wait_for(lambda _driver: click_button(btn_css)) - - assert_false(world.css_find(btn_css).visible) + confirm_studio_prompt() @step(u'I am shown a prompt$') diff --git a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py index bcd3dadff1..0c5565503e 100644 --- a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py +++ b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py @@ -23,8 +23,7 @@ def create_component_instance(step, category, component_type=None, is_advanced=F ---------- category: component type (discussion, html, problem, video) component_type: for components with multiple templates, the link text in the menu - is_advanced: for html and problem, is the desired component under the - advanced menu + is_advanced: for problems, is the desired component under the advanced menu? """ assert_in(category, ['problem', 'html', 'video', 'discussion']) @@ -40,6 +39,8 @@ def create_component_instance(step, category, component_type=None, is_advanced=F # 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'): @@ -50,17 +51,13 @@ def create_component_instance(step, category, component_type=None, is_advanced=F module_count_before + 1)) -@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) - my_css = 'ul.problem-type-tabs li.ui-state-active a[href="#tab2"]' - assert(world.css_find(my_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_link(category, component_type): diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py index 4ccac98020..33770edcbe 100644 --- a/cms/djangoapps/contentstore/features/course-team.py +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -2,22 +2,24 @@ #pylint: disable=W0621 from lettuce import world, step -from auth.authz import get_course_groupname_for_role, get_user_by_email -from nose.tools import assert_true, assert_in # pylint: disable=E0611 +from nose.tools import assert_in # pylint: disable=E0611 -@step(u'(I am viewing|s?he views) the course team settings') +@step(u'(I am viewing|s?he views) the course team settings$') def view_grading_settings(_step, whom): world.click_course_settings() link_css = 'li.nav-course-settings-team a' world.css_click(link_css) -@step(u'I add "([^"]*)" to the course team') +@step(u'I add "([^"]*)" to the course team$') def add_other_user(_step, name): new_user_css = 'a.create-user-button' world.css_click(new_user_css) - world.wait(0.5) + + # Wait for the css animation to apply the is-shown class + shown_css = 'div.wrapper-create-user.is-shown' + world.wait_for_present(shown_css) email_css = 'input#user-email-input' world.css_fill(email_css, name + '@edx.org') @@ -27,35 +29,30 @@ def add_other_user(_step, name): world.css_click(confirm_css) -@step(u'I delete "([^"]*)" from the course team') +@step(u'I delete "([^"]*)" from the course team$') def delete_other_user(_step, name): to_delete_css = '.user-item .item-actions a.remove-user[data-id="{email}"]'.format( email="{0}{1}".format(name, '@edx.org')) world.css_click(to_delete_css) - # confirm prompt - # need to wait for the animation to be done, there isn't a good success condition that won't work both on latest chrome and jenkins - world.wait(.5) - world.css_click(".wrapper-prompt-warning .action-primary") + world.confirm_studio_prompt() -@step(u's?he deletes me from the course team') +@step(u's?he deletes me from the course team$') def other_delete_self(_step): to_delete_css = '.user-item .item-actions a.remove-user[data-id="{email}"]'.format( email="robot+studio@edx.org") world.css_click(to_delete_css) - # confirm prompt - world.wait(.5) - world.css_click(".wrapper-prompt-warning .action-primary") + world.confirm_studio_prompt() -@step(u'I make "([^"]*)" a course team admin') +@step(u'I make "([^"]*)" a course team admin$') def make_course_team_admin(_step, name): admin_btn_css = '.user-item[data-email="{name}@edx.org"] .user-actions .add-admin-role'.format( name=name) world.css_click(admin_btn_css) -@step(u'I remove admin rights from ("([^"]*)"|myself)') +@step(u'I remove admin rights from ("([^"]*)"|myself)$') def remove_course_team_admin(_step, outer_capture, name): if outer_capture == "myself": email = world.scenario_dict["USER"].email @@ -66,8 +63,8 @@ def remove_course_team_admin(_step, outer_capture, name): world.css_click(admin_btn_css) -@step(u'I( do not)? see the course on my page') -@step(u's?he does( not)? see the course on (his|her) page') +@step(u'I( do not)? see the course on my page$') +@step(u's?he does( not)? see the course on (his|her) page$') def see_course(_step, do_not_see, gender='self'): class_css = 'h3.course-title' if do_not_see: @@ -78,7 +75,7 @@ def see_course(_step, do_not_see, gender='self'): assert_in(world.scenario_dict['COURSE'].display_name, all_names) -@step(u'"([^"]*)" should( not)? be marked as an admin') +@step(u'"([^"]*)" should( not)? be marked as an admin$') def marked_as_admin(_step, name, not_marked_admin): flag_css = '.user-item[data-email="{name}@edx.org"] .flag-role.flag-role-admin'.format( name=name) @@ -88,13 +85,13 @@ def marked_as_admin(_step, name, not_marked_admin): assert world.is_css_present(flag_css) -@step(u'I should( not)? be marked as an admin') +@step(u'I should( not)? be marked as an admin$') def self_marked_as_admin(_step, not_marked_admin): return marked_as_admin(_step, "robot+studio", not_marked_admin) -@step(u'I can(not)? delete users') -@step(u's?he can(not)? delete users') +@step(u'I can(not)? delete users$') +@step(u's?he can(not)? delete users$') def can_delete_users(_step, can_not_delete): to_delete_css = 'a.remove-user' if can_not_delete: @@ -103,8 +100,8 @@ def can_delete_users(_step, can_not_delete): assert world.is_css_present(to_delete_css) -@step(u'I can(not)? add users') -@step(u's?he can(not)? add users') +@step(u'I can(not)? add users$') +@step(u's?he can(not)? add users$') def can_add_users(_step, can_not_add): add_css = 'a.create-user-button' if can_not_add: @@ -113,8 +110,8 @@ def can_add_users(_step, can_not_add): assert world.is_css_present(add_css) -@step(u'I can(not)? make ("([^"]*)"|myself) a course team admin') -@step(u's?he can(not)? make ("([^"]*)"|me) a course team admin') +@step(u'I can(not)? make ("([^"]*)"|myself) a course team admin$') +@step(u's?he can(not)? make ("([^"]*)"|me) a course team admin$') def can_make_course_admin(_step, can_not_make_admin, outer_capture, name): if outer_capture == "myself": email = world.scenario_dict["USER"].email diff --git a/cms/djangoapps/contentstore/features/problem-editor.py b/cms/djangoapps/contentstore/features/problem-editor.py index ef8a3b5a4a..3808458973 100644 --- a/cms/djangoapps/contentstore/features/problem-editor.py +++ b/cms/djangoapps/contentstore/features/problem-editor.py @@ -6,7 +6,7 @@ from lettuce import world, step from nose.tools import assert_equal, assert_true # pylint: disable=E0611 from common import type_in_codemirror, open_new_course from course_import import import_file, go_to_import - +from selenium.webdriver.common.keys import Keys DISPLAY_NAME = "Display Name" MAXIMUM_ATTEMPTS = "Maximum Attempts" @@ -47,9 +47,7 @@ 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.css_fill('.wrapper-comp-setting .setting-input', '3.4', index=index) - if world.is_firefox(): - world.trigger_event('.wrapper-comp-setting .setting-input', index=index) + set_field_value(index, '3.4') verify_modified_display_name() @@ -62,9 +60,7 @@ def my_display_name_change_is_persisted_on_save(step): @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.css_fill('.wrapper-comp-setting .setting-input', "updated ' \" &", index=index) - if world.is_firefox(): - world.trigger_event('.wrapper-comp-setting .setting-input', index=index) + set_field_value(index, "updated ' \" &") verify_modified_display_name_with_special_chars() @@ -136,11 +132,10 @@ def set_the_weight_to_abc(step, bad_weight): @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 behaviour is different. eg 2.34 displays as 2.34 and is persisted as 2 + # on firefox with selenium, the behaviour is different. + # eg 2.34 displays as 2.34 and is persisted as 2 index = world.get_setting_entry_index(MAXIMUM_ATTEMPTS) - world.css_fill('.wrapper-comp-setting .setting-input', max_attempts_set, index=index) - if world.is_firefox(): - world.trigger_event('.wrapper-comp-setting .setting-input', index=index) + 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" @@ -276,12 +271,23 @@ def verify_unset_display_name(): world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, 'Blank Advanced Problem', False) +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.setting-input')[index] + elem.value = value + elem.type(Keys.TAB) + + def set_weight(weight): index = world.get_setting_entry_index(PROBLEM_WEIGHT) - world.css_fill('.wrapper-comp-setting .setting-input', weight, index=index) - if world.is_firefox(): - world.trigger_event('.wrapper-comp-setting .setting-input', index=index, event='blur') - world.trigger_event('a.save-button', event='focus') + set_field_value(index, weight) def open_high_level_source(): diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py index 22a4425686..c68636b583 100644 --- a/cms/djangoapps/contentstore/features/upload.py +++ b/cms/djangoapps/contentstore/features/upload.py @@ -88,11 +88,7 @@ def delete_file(_step, file_name): assert index != -1 delete_css = "a.remove-asset-button" world.css_click(delete_css, index=index) - - world.wait_for_present(".wrapper-prompt.is-shown") - world.wait(0.2) # wait for css animation - prompt_confirm_css = 'li.nav-item > a.action-primary' - world.css_click(prompt_confirm_css) + world.confirm_studio_prompt() @step(u'I should see only one "([^"]*)"$') diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index ac8a3a2d1a..92e7a0425b 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -223,7 +223,20 @@ def wait_for_ajax_complete(): } }, 100); """ - world.browser.driver.execute_async_script(dedent(javascript)) + # 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 @@ -268,9 +281,9 @@ def css_has_text(css_selector, text, index=0, strip=False): # If we're expecting a non-empty string, give the page # a chance to fill in text fields. if text: - world.wait_for(lambda _: world.css_text(css_selector, index=index)) + wait_for(lambda _: css_text(css_selector, index=index)) - actual_text = world.css_text(css_selector, index=index) + actual_text = css_text(css_selector, index=index) if strip: actual_text = actual_text.strip() @@ -291,88 +304,76 @@ def css_has_value(css_selector, value, index=0): # If we're expecting a non-empty string, give the page # a chance to fill in values if value: - world.wait_for(lambda _: world.css_value(css_selector, index=index)) + wait_for(lambda _: css_value(css_selector, index=index)) - return world.css_value(css_selector, index=index) == value + return css_value(css_selector, index=index) == value @world.absorb -def wait_for(func, timeout=5): - WebDriverWait( - driver=world.browser.driver, - timeout=timeout, - ignored_exceptions=(StaleElementReferenceException) - ).until(func) +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=30): """ - Waiting for the element to be present in the DOM. - Throws an error if the WebDriverWait timeout clock expires. - Otherwise this method will return None + Wait for the element to be present in the DOM. """ - try: - WebDriverWait( - driver=world.browser.driver, - timeout=timeout, - ignored_exceptions=(StaleElementReferenceException) - ).until(EC.presence_of_element_located((By.CSS_SELECTOR, css_selector,))) - except TimeoutException: - raise TimeoutException("Timed out waiting for {} to be present.".format(css_selector)) + 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, timeout=30): +def wait_for_visible(css_selector, index=0, timeout=30): """ - Waiting for the element to be visible in the DOM. - Throws an error if the WebDriverWait timeout clock expires. - Otherwise this method will return None + Wait for the element to be visible in the DOM. """ - try: - WebDriverWait( - driver=world.browser.driver, - timeout=timeout, - ignored_exceptions=(StaleElementReferenceException) - ).until(EC.visibility_of_element_located((By.CSS_SELECTOR, css_selector,))) - except TimeoutException: - raise TimeoutException("Timed out waiting for {} to be visible.".format(css_selector)) + 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=30): """ - Waiting for the element to be either invisible or not present on the DOM. - Throws an error if the WebDriverWait timeout clock expires. - Otherwise this method will return None + Wait for the element to be either invisible or not present on the DOM. """ - try: - WebDriverWait( - driver=world.browser.driver, - timeout=timeout, - ignored_exceptions=(StaleElementReferenceException) - ).until(EC.invisibility_of_element_located((By.CSS_SELECTOR, css_selector,))) - except TimeoutException: - raise TimeoutException("Timed out waiting for {} to be invisible.".format(css_selector)) + 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=30): """ - Waiting for the element to be present and clickable. - Throws an error if the WebDriverWait timeout clock expires. - Otherwise this method will return None. + Wait for the element to be present and clickable. """ - # Sometimes the element is clickable then gets obscured. - # In this case, pause so that it is not reported clickable too early - try: - WebDriverWait( - driver=world.browser.driver, - timeout=timeout, - ignored_exceptions=(StaleElementReferenceException) - ).until(EC.element_to_be_clickable((By.CSS_SELECTOR, css_selector,))) - except TimeoutException: - raise TimeoutException("Timed out waiting for {} to be clickable.".format(css_selector)) + 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 @@ -396,40 +397,47 @@ def css_click(css_selector, index=0, wait_time=30): This method will return True if the click worked. """ wait_for_clickable(css_selector, timeout=wait_time) + wait_for_visible(css_selector, index=index, timeout=wait_time) assert_true( - world.css_visible(css_selector, index=index), + css_visible(css_selector, index=index), msg="Element {}[{}] is present but not visible".format(css_selector, index) ) - result = retry_on_exception(lambda: world.css_find(css_selector)[index].click()) + result = retry_on_exception(lambda: css_find(css_selector)[index].click()) if result: wait_for_js_to_load() return result @world.absorb -def css_check(css_selector, index=0, wait_time=30): +def css_check(css_selector, wait_time=30): """ 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. """ - return css_click(css_selector=css_selector, index=index, wait_time=wait_time) + 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, index=0, wait_time=30): +def select_option(name, value, wait_time=30): ''' 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) - return css_click(css_selector=css_selector, index=index, wait_time=wait_time) + css_click(css_selector=css_selector, wait_time=wait_time) + wait_for(lambda _: css_has_value(select_css, value)) + return True @world.absorb @@ -442,7 +450,15 @@ def id_click(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 @@ -512,19 +528,19 @@ def save_the_html(path='/tmp'): @world.absorb def click_course_content(): course_content_css = 'li.nav-course-courseware' - world.css_click(course_content_css) + css_click(course_content_css) @world.absorb def click_course_settings(): course_settings_css = 'li.nav-course-settings' - world.css_click(course_settings_css) + css_click(course_settings_css) @world.absorb def click_tools(): tools_css = 'li.nav-course-tools' - world.css_click(tools_css) + css_click(tools_css) @world.absorb diff --git a/lms/djangoapps/courseware/features/certificates.py b/lms/djangoapps/courseware/features/certificates.py index bcfc1a120b..b8a5b958b6 100644 --- a/lms/djangoapps/courseware/features/certificates.py +++ b/lms/djangoapps/courseware/features/certificates.py @@ -78,6 +78,7 @@ def select_the_verified_track(step): create_cert_course() register() select_contribution(32) + world.wait_for_ajax_complete() btn_css = 'input[value="Select Certificate"]' world.css_click(btn_css) assert world.is_css_present('section.progress') @@ -174,6 +175,9 @@ def at_the_payment_page(step): @step(u'I submit valid payment information$') def submit_payment(step): + # First make sure that the page is done if it still executing + # an ajax query. + world.wait_for_ajax_complete() button_css = 'input[value=Submit]' world.css_click(button_css) diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature index 0dca3201c1..291490903c 100644 --- a/lms/djangoapps/courseware/features/problems.feature +++ b/lms/djangoapps/courseware/features/problems.feature @@ -180,7 +180,6 @@ Feature: LMS.Answer problems Given I am viewing a "" problem Then my "" answer is marked "unanswered" When I answer a "" problem "ly" - And I wait for "1" seconds And I input an answer on a "" problem "ly" Then my "" answer is marked "unanswered" And I reset the problem @@ -208,7 +207,6 @@ Feature: LMS.Answer problems Scenario: I can reset the correctness of a radiogroup problem after changing my answer Given I am viewing a "" problem When I answer a "" problem "ly" - And I wait for "1" seconds Then my "" answer is marked "" And I input an answer on a "" problem "ly" Then my "" answer is NOT marked "" diff --git a/lms/djangoapps/courseware/features/problems_setup.py b/lms/djangoapps/courseware/features/problems_setup.py index a705a8467c..c403890557 100644 --- a/lms/djangoapps/courseware/features/problems_setup.py +++ b/lms/djangoapps/courseware/features/problems_setup.py @@ -163,6 +163,10 @@ PROBLEM_DICT = { def answer_problem(problem_type, correctness): + # Make sure that the problem has been completely rendered before + # starting to input an answer. + world.wait_for_ajax_complete() + if problem_type == "drop down": select_name = "input_i4x-edx-model_course-problem-drop_down_2_1" option_text = 'Option 2' if correctness == 'correct' else 'Option 3'