diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 992de9301c..8a34a2faea 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -2,7 +2,7 @@ # pylint: disable=W0621 from lettuce import world, step -from nose.tools import assert_true, assert_equal, assert_in, assert_false # pylint: disable=E0611 +from nose.tools import assert_true, assert_in, assert_false # pylint: disable=E0611 from auth.authz import get_user_by_email, get_course_groupname_for_role from django.conf import settings @@ -224,14 +224,50 @@ def i_enabled_the_advanced_module(step, module): press_the_notification_button(step, 'Save') -@step('I have clicked the new unit button') -def open_new_unit(step): - step.given('I have opened a new course section in Studio') - step.given('I have added a new subsection') - step.given('I expand the first section') - old_url = world.browser.url - world.css_click('a.new-unit-item') - world.wait_for(lambda x: world.browser.url != old_url) +@world.absorb +def create_course_with_unit(): + """ + Prepare for tests by creating a course with a section, subsection, and unit. + Performs the following: + Clear out all courseware + Create a course with a section, subsection, and unit + Create a user and make that user a course author + Log the user into studio + Open the course from the dashboard + Expand the section and click on the New Unit link + The end result is the page where the user is editing the new unit + """ + 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') + + css_selectors = [ + 'div.section-item a.expand-collapse-icon', 'a.new-unit-item' + ] + for selector in css_selectors: + world.css_click(selector) + + world.wait_for_mathjax() + world.wait_for_xmodule() + + assert world.is_css_present('ul.new-component-type') + + +@step('I have clicked the new unit button$') +@step(u'I am in Studio editing a new unit$') +def edit_new_unit(step): + create_course_with_unit() @step('the save notification button is disabled') @@ -267,9 +303,9 @@ def confirm_the_prompt(step): assert_false(world.css_find(btn_css).visible) -@step(u'I am shown a (.*)$') -def i_am_shown_a_notification(step, notification_type): - assert world.is_css_present('.wrapper-%s' % notification_type) +@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): diff --git a/cms/djangoapps/contentstore/features/component.feature b/cms/djangoapps/contentstore/features/component.feature index 3877cccc55..7f31eb6d69 100644 --- a/cms/djangoapps/contentstore/features/component.feature +++ b/cms/djangoapps/contentstore/features/component.feature @@ -80,9 +80,3 @@ Feature: CMS.Component Adding And I add a "Blank Advanced Problem" "Advanced Problem" component And I delete all components Then I see no components - - Scenario: I see a notification on save - Given I am in Studio editing a new unit - And I add a "Discussion" "single step" component - And I edit and save a component - Then I am shown a notification diff --git a/cms/djangoapps/contentstore/features/component.py b/cms/djangoapps/contentstore/features/component.py index af82b1ff7a..f8425a3600 100644 --- a/cms/djangoapps/contentstore/features/component.py +++ b/cms/djangoapps/contentstore/features/component.py @@ -2,52 +2,19 @@ #pylint: disable=W0621 from lettuce import world, step -from nose.tools import assert_true, assert_in, assert_equal # pylint: disable=E0611 -from common import create_studio_user, add_course_author, log_into_studio - - -@step(u'I am in Studio editing a new unit$') -def add_unit(step): - world.clear_courses() - course = world.CourseFactory.create() - 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.wait_for_requirejs([ - "jquery", "gettext", "js/models/course", "coffee/src/models/module", - "coffee/src/views/unit", "jquery.ui", - ]) - world.wait_for_mathjax() - css_selectors = [ - 'a.course-link', 'div.section-item a.expand-collapse-icon', - 'a.new-unit-item', - ] - for selector in css_selectors: - world.css_click(selector) +from nose.tools import assert_true, assert_in # pylint: disable=E0611 @step(u'I add this type of single step component:$') def add_a_single_step_component(step): - world.wait_for_xmodule() for step_hash in step.hashes: component = step_hash['Component'] assert_in(component, ['Discussion', 'Video']) - css_selector = 'a[data-type="{}"]'.format(component.lower()) - world.css_click(css_selector) - # In the current implementation, all the "new component" - # buttons are handled by one BackBone.js view. - # If we click two buttons at super-human speed, - # the view will miss the second click while it's - # processing the first. - # To account for this, we wait for each component - # to be created before clicking the next component. - world.wait_for_visible('section.xmodule_{}Module'.format(component)) + world.create_component_instance( + step=step, + category='{}'.format(component.lower()), + ) @step(u'I see this type of single step component:$') @@ -62,45 +29,13 @@ def see_a_single_step_component(step): @step(u'I add this type of( Advanced)? (HTML|Problem) component:$') def add_a_multi_step_component(step, is_advanced, category): - 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)) - - def find_matching_link(): - """ - Find the link with the specified text. There should be one and only one. - """ - # The tab shows links for the given category - links = world.css_find('div.new-component-{} a'.format(category)) - - # Find the link whose text matches what you're looking for - matched_links = [link for link in links if link.text == step_hash['Component']] - - # There should be one and only one - assert_equal(len(matched_links), 1) - return matched_links[0] - - def click_link(): - link.click() - - world.wait_for_xmodule() - category = category.lower() for step_hash in step.hashes: - css_selector = 'a[data-type="{}"]'.format(category) - world.css_click(css_selector) - world.wait_for_invisible(css_selector) - - if is_advanced: - # Sometimes this click does not work if you go too fast. - world.retry_on_exception(click_advanced, max_attempts=5, ignored_exceptions=AssertionError) - - # Retry this in case the list is empty because you tried too fast. - link = world.retry_on_exception(func=find_matching_link, ignored_exceptions=AssertionError) - - # Wait for the link to be clickable. If you go too fast it is not. - world.retry_on_exception(click_link) + 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:') diff --git a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py index 63dba91a15..d816e6e4bb 100644 --- a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py +++ b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py @@ -2,30 +2,35 @@ #pylint: disable=C0111 from lettuce import world -from nose.tools import assert_equal, assert_true # pylint: disable=E0611 +from nose.tools import assert_equal, assert_true, assert_in # pylint: disable=E0611 from terrain.steps import reload_the_page @world.absorb -def create_component_instance(step, component_button_css, category, - expected_css, boilerplate=None, - has_multiple_templates=True): +def create_component_instance(step, category, component_type=None, is_advanced=False): + """ + Create a new component in a Unit. - click_new_component_button(step, component_button_css) + Parameters + ---------- + 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 + """ + assert_in(category, ['problem', 'html', 'video', 'discussion']) + + component_button_css = '.large-{}-icon'.format(category.lower()) + world.css_click(component_button_css) if category in ('problem', 'html'): + world.wait_for_invisible(component_button_css) + click_component_from_menu(category, component_type, is_advanced) - def animation_done(_driver): - script = "$('div.new-component').css('display')" - return world.browser.evaluate_script(script) == 'none' - - world.wait_for(animation_done) - - if has_multiple_templates: - click_component_from_menu(category, boilerplate, expected_css) - - if category in ('video',): - world.wait_for_xmodule() + if category == 'problem': + expected_css = 'section.xmodule_CapaModule' + else: + expected_css = 'section.xmodule_{}Module'.format(category.title()) assert_true(world.is_css_present(expected_css)) @@ -33,29 +38,53 @@ def create_component_instance(step, component_button_css, category, @world.absorb def click_new_component_button(step, component_button_css): step.given('I have clicked the new unit button') - world.wait_for_requirejs( - ["jquery", "js/models/course", "coffee/src/models/module", - "coffee/src/views/unit", "jquery.ui", "domReady!"] - ) world.css_click(component_button_css) -@world.absorb -def click_component_from_menu(category, boilerplate, expected_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)) + + +def _find_matching_link(category, component_type): """ - Creates a component from `instance_id`. For components with more - than one template, clicks on `elem_css` to create the new - component. Components with only one template are created as soon - as the user clicks the appropriate button, so we assert that the - expected component is present. + Find the link with the specified text. There should be one and only one. """ - if boilerplate: - elem_css = "a[data-category='{}'][data-boilerplate='{}']".format(category, boilerplate) - else: - elem_css = "a[data-category='{}']:not([data-boilerplate])".format(category) - elements = world.css_find(elem_css) - assert_equal(len(elements), 1) - world.css_click(elem_css) + + # The tab shows links for the given category + links = world.css_find('div.new-component-{} a'.format(category)) + + # Find the link whose text matches what you're looking for + matched_links = [link for link in links if link.text == component_type] + + # There should be one and only one + assert_equal(len(matched_links), 1) + return matched_links[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_link(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 diff --git a/cms/djangoapps/contentstore/features/course-overview.feature b/cms/djangoapps/contentstore/features/course-overview.feature index 80b400a58e..a10237de5d 100644 --- a/cms/djangoapps/contentstore/features/course-overview.feature +++ b/cms/djangoapps/contentstore/features/course-overview.feature @@ -58,20 +58,3 @@ Feature: CMS.Course Overview And I click the "Expand All Sections" link Then I see the "Collapse All Sections" link And all sections are expanded - - Scenario: Notification is shown on grading status changes - Given I have a course with 1 section - When I navigate to the course overview page - And I change an assignment's grading status - Then I am shown a notification - - # Notification is not shown on reorder for IE - # Safari does not have moveMouseTo implemented - @skip_internetexplorer - @skip_safari - Scenario: Notification is shown on subsection reorder - Given I have opened a new course section in Studio - And I have added a new subsection - And I have added a new subsection - When I reorder subsections - Then I am shown a notification diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py index deba2d820d..4ccac98020 100644 --- a/cms/djangoapps/contentstore/features/course-team.py +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -50,8 +50,8 @@ def other_delete_self(_step): @step(u'I make "([^"]*)" a course team admin') def make_course_team_admin(_step, name): - admin_btn_css = '.user-item[data-email="{email}"] .user-actions .add-admin-role'.format( - email=name+'@edx.org') + admin_btn_css = '.user-item[data-email="{name}@edx.org"] .user-actions .add-admin-role'.format( + name=name) world.css_click(admin_btn_css) @@ -80,8 +80,8 @@ def see_course(_step, do_not_see, gender='self'): @step(u'"([^"]*)" should( not)? be marked as an admin') def marked_as_admin(_step, name, not_marked_admin): - flag_css = '.user-item[data-email="{email}"] .flag-role.flag-role-admin'.format( - email=name+'@edx.org') + flag_css = '.user-item[data-email="{name}@edx.org"] .flag-role.flag-role-admin'.format( + name=name) if not_marked_admin: assert world.is_css_not_present(flag_css) else: diff --git a/cms/djangoapps/contentstore/features/course_import.py b/cms/djangoapps/contentstore/features/course_import.py index 0d26124d79..84b7affe30 100644 --- a/cms/djangoapps/contentstore/features/course_import.py +++ b/cms/djangoapps/contentstore/features/course_import.py @@ -2,6 +2,7 @@ import os from lettuce import world from django.conf import settings + 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) diff --git a/cms/djangoapps/contentstore/features/discussion-editor.feature b/cms/djangoapps/contentstore/features/discussion-editor.feature index 7278accf0b..17904e8820 100644 --- a/cms/djangoapps/contentstore/features/discussion-editor.feature +++ b/cms/djangoapps/contentstore/features/discussion-editor.feature @@ -2,7 +2,7 @@ Feature: CMS.Discussion Component Editor As a course author, I want to be able to create discussion components. - Scenario: User can view metadata + Scenario: User can view discussion component metadata Given I have created a Discussion Tag And I edit and select Settings Then I see three alphabetized settings and their expected values @@ -14,7 +14,3 @@ Feature: CMS.Discussion Component Editor And I edit and select Settings Then I can modify the display name And my display name change is persisted on save - - Scenario: Creating a discussion takes a single click - Given I have clicked the new unit button - Then creating a discussion takes a single click diff --git a/cms/djangoapps/contentstore/features/discussion-editor.py b/cms/djangoapps/contentstore/features/discussion-editor.py index d68860ff49..0bae84459f 100644 --- a/cms/djangoapps/contentstore/features/discussion-editor.py +++ b/cms/djangoapps/contentstore/features/discussion-editor.py @@ -6,11 +6,10 @@ from lettuce import world, step @step('I have created a Discussion Tag$') def i_created_discussion_tag(step): + world.create_course_with_unit() world.create_component_instance( - step, '.large-discussion-icon', - 'discussion', - '.xmodule_DiscussionModule', - has_multiple_templates=False + step=step, + category='discussion', ) @@ -22,12 +21,3 @@ def i_see_only_the_settings_and_values(step): ['Display Name', "Discussion", False], ['Subcategory', "Topic-Level Student-Visible Label", False] ]) - - -@step('creating a discussion takes a single click') -def discussion_takes_a_single_click(step): - component_css = '.xmodule_DiscussionModule' - assert world.is_css_not_present(component_css) - - world.css_click("a[data-category='discussion']") - assert world.is_css_present(component_css) diff --git a/cms/djangoapps/contentstore/features/grading.py b/cms/djangoapps/contentstore/features/grading.py index b0db396081..dcc11857d8 100644 --- a/cms/djangoapps/contentstore/features/grading.py +++ b/cms/djangoapps/contentstore/features/grading.py @@ -180,7 +180,7 @@ def cannot_edit_fail(_step): def i_change_grace_period(_step, grace_period): grace_period_css = '#course-grading-graceperiod' ele = world.css_find(grace_period_css).first - + # Sometimes it takes a moment for the JavaScript # to populate the field. If we don't wait for # this to happen, then we can end up with diff --git a/cms/djangoapps/contentstore/features/html-editor.py b/cms/djangoapps/contentstore/features/html-editor.py index d89f052dcc..f00db67c07 100644 --- a/cms/djangoapps/contentstore/features/html-editor.py +++ b/cms/djangoapps/contentstore/features/html-editor.py @@ -6,9 +6,11 @@ from lettuce import world, step @step('I have created a Blank HTML Page$') def i_created_blank_html_page(step): + world.create_course_with_unit() world.create_component_instance( - step, '.large-html-icon', 'html', - '.xmodule_HtmlModule' + step=step, + category='html', + component_type='Text' ) @@ -18,11 +20,10 @@ def i_see_only_the_html_display_name(step): @step('I have created an E-text Written in LaTeX$') -def i_created_blank_html_page(step): +def i_created_etext_in_latex(step): + world.create_course_with_unit() world.create_component_instance( - step, - '.large-html-icon', - 'html', - '.xmodule_HtmlModule', - 'latex_html.yaml' + step=step, + category='html', + component_type='E-text Written in LaTeX' ) diff --git a/cms/djangoapps/contentstore/features/problem-editor.py b/cms/djangoapps/contentstore/features/problem-editor.py index 262d1bfc10..ef8a3b5a4a 100644 --- a/cms/djangoapps/contentstore/features/problem-editor.py +++ b/cms/djangoapps/contentstore/features/problem-editor.py @@ -1,7 +1,6 @@ # disable missing docstring #pylint: disable=C0111 -import os import json from lettuce import world, step from nose.tools import assert_equal, assert_true # pylint: disable=E0611 @@ -18,12 +17,11 @@ SHOW_ANSWER = "Show Answer" @step('I have created a Blank Common Problem$') def i_created_blank_common_problem(step): + world.create_course_with_unit() world.create_component_instance( - step, - '.large-problem-icon', - 'problem', - '.xmodule_CapaModule', - 'blank_common.yaml' + step=step, + category='problem', + component_type='Blank Common Problem' ) @@ -168,14 +166,13 @@ def cancel_does_not_save_changes(step): @step('I have created a LaTeX Problem') def create_latex_problem(step): - world.click_new_component_button(step, '.large-problem-icon') - - def animation_done(_driver): - return world.browser.evaluate_script("$('div.new-component').css('display')") == 'none' - world.wait_for(animation_done) - # Go to advanced tab. - world.css_click('#ui-id-2') - world.click_component_from_menu("problem", "latex_problem.yaml", '.xmodule_CapaModule') + world.create_course_with_unit() + 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') diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py index 3fea8637c6..b6f55969bb 100644 --- a/cms/djangoapps/contentstore/features/section.py +++ b/cms/djangoapps/contentstore/features/section.py @@ -5,8 +5,6 @@ from lettuce import world, step from common import * from nose.tools import assert_equal # pylint: disable=E0611 -############### ACTIONS #################### - @step('I click the New Section link$') def i_click_new_section_link(_step): @@ -53,9 +51,6 @@ def i_see_a_mini_notification(_step, _type): assert world.is_css_present(saving_css) -############ ASSERTIONS ################### - - @step('I see my section on the Courseware page$') def i_see_my_section_on_the_courseware_page(_step): see_my_section_on_the_courseware_page('My Section') @@ -125,8 +120,6 @@ def the_section_release_date_is_updated(_step): assert_equal(status_text, 'Will Release: 12/25/2013 at 00:00 UTC') -############ HELPER METHODS ################### - def save_section_name(name): name_css = '.new-section-name' save_css = '.new-section-name-save' diff --git a/cms/djangoapps/contentstore/features/textbooks.py b/cms/djangoapps/contentstore/features/textbooks.py index 2cf6683d6d..bac4e6bebc 100644 --- a/cms/djangoapps/contentstore/features/textbooks.py +++ b/cms/djangoapps/contentstore/features/textbooks.py @@ -47,7 +47,7 @@ def name_textbook(_step, name): @step(u'I name the (first|second|third) chapter "([^"]*)"') def name_chapter(_step, ordinal, name): index = ["first", "second", "third"].index(ordinal) - input_css = ".textbook .chapter{i} input.chapter-name".format(i=index+1) + input_css = ".textbook .chapter{i} input.chapter-name".format(i=index + 1) world.css_fill(input_css, name) if world.is_firefox(): world.trigger_event(input_css) @@ -56,7 +56,7 @@ def name_chapter(_step, ordinal, name): @step(u'I type in "([^"]*)" for the (first|second|third) chapter asset') def asset_chapter(_step, name, ordinal): index = ["first", "second", "third"].index(ordinal) - input_css = ".textbook .chapter{i} input.chapter-asset-path".format(i=index+1) + input_css = ".textbook .chapter{i} input.chapter-asset-path".format(i=index + 1) world.css_fill(input_css, name) if world.is_firefox(): world.trigger_event(input_css) @@ -65,7 +65,7 @@ def asset_chapter(_step, name, ordinal): @step(u'I click the Upload Asset link for the (first|second|third) chapter') def click_upload_asset(_step, ordinal): index = ["first", "second", "third"].index(ordinal) - button_css = ".textbook .chapter{i} .action-upload".format(i=index+1) + button_css = ".textbook .chapter{i} .action-upload".format(i=index + 1) world.css_click(button_css) diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py index cf0d92fc94..22a4425686 100644 --- a/cms/djangoapps/contentstore/features/upload.py +++ b/cms/djangoapps/contentstore/features/upload.py @@ -191,7 +191,7 @@ def view_asset(_step, status): # Note that world.visit would trigger a 403 error instead of displaying "Unauthorized" # Instead, we can drop back into the selenium driver get command. world.browser.driver.get(url) - assert_equal(world.css_text('body'),expected_text) + assert_equal(world.css_text('body'), expected_text) @step('I see a confirmation that the file was deleted$') diff --git a/cms/djangoapps/contentstore/features/video.py b/cms/djangoapps/contentstore/features/video.py index 12f2992568..80963ab556 100644 --- a/cms/djangoapps/contentstore/features/video.py +++ b/cms/djangoapps/contentstore/features/video.py @@ -12,11 +12,10 @@ BUTTONS = { @step('I have created a Video component$') def i_created_a_video_component(step): + world.create_course_with_unit() world.create_component_instance( - step, '.large-video-icon', - 'video', - '.xmodule_VideoModule', - has_multiple_templates=False + step=step, + category='video', ) @@ -155,4 +154,3 @@ def check_captions_visibility_state(_step, visibility_state, timeout): assert world.css_visible('.subtitles') else: assert not world.css_visible('.subtitles') - diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index f9e9d93fc2..86f0419849 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -11,7 +11,6 @@ # Disable the "unused argument" warning because lettuce uses "step" #pylint: disable=W0613 -import re from lettuce import world, step from .course_helpers import * from .ui_helpers import * @@ -23,32 +22,15 @@ logger = getLogger(__name__) @step(r'I wait (?:for )?"(\d+\.?\d*)" seconds?$') -def wait(step, seconds): +def wait_for_seconds(step, seconds): world.wait(seconds) -REQUIREJS_WAIT = { - re.compile('settings-details'): [ - "jquery", "js/models/course", - "js/models/settings/course_details", "js/views/settings/main"], - re.compile('settings-advanced'): [ - "jquery", "js/models/course", "js/models/settings/advanced", - "js/views/settings/advanced", "codemirror"], - re.compile('edit\/.+vertical'): [ - "jquery", "js/models/course", "coffee/src/models/module", - "coffee/src/views/unit", "jquery.ui"], -} - @step('I reload the page$') def reload_the_page(step): world.wait_for_ajax_complete() world.browser.reload() - requirements = None - for test, req in REQUIREJS_WAIT.items(): - if test.search(world.browser.url): - requirements = req - break - world.wait_for_requirejs(requirements) + world.wait_for_js_to_load() @step('I press the browser back button$') @@ -163,9 +145,9 @@ def should_see_in_the_page(step, doesnt_appear, text): else: multiplier = 1 if doesnt_appear: - assert world.browser.is_text_not_present(text, wait_time=5*multiplier) + assert world.browser.is_text_not_present(text, wait_time=5 * multiplier) else: - assert world.browser.is_text_present(text, wait_time=5*multiplier) + assert world.browser.is_text_present(text, wait_time=5 * multiplier) @step('I am logged in$') diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index f973bcb4ac..be52c4bd3d 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -4,11 +4,13 @@ from lettuce import world import time import json +import re import platform from textwrap import dedent from urllib import quote_plus from selenium.common.exceptions import ( - WebDriverException, TimeoutException, StaleElementReferenceException) + WebDriverException, TimeoutException, + StaleElementReferenceException) from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait @@ -16,11 +18,50 @@ from lettuce.django import django_url from nose.tools import assert_true # pylint: disable=E0611 +REQUIREJS_WAIT = { + # Settings - Schedule & Details + re.compile('^Schedule & Details Settings \|'): [ + "jquery", "js/models/course", + "js/models/settings/course_details", "js/views/settings/main"], + + # Settings - Advanced Settings + re.compile('^Advanced Settings \|'): [ + "jquery", "js/models/course", "js/models/settings/advanced", + "js/views/settings/advanced", "codemirror"], + + # Individual Unit (editing) + re.compile('^Individual Unit \|'): [ + "coffee/src/models/module", "coffee/src/views/unit", + "coffee/src/views/module_edit"], + + # Content - Outline + # Note that calling your org, course number, or display name, 'course' will mess this up + re.compile('^Course Outline \|'): [ + "js/models/course", "js/models/location", "js/models/section", + "js/views/overview", "js/views/section_edit"], + + # Dashboard + re.compile('^My Courses \|'): [ + "js/sock", "gettext", "js/base", + "jquery.ui", "coffee/src/main", "underscore"], +} + + @world.absorb def wait(seconds): time.sleep(float(seconds)) +@world.absorb +def wait_for_js_to_load(): + requirements = None + for test, req in REQUIREJS_WAIT.items(): + if test.search(world.browser.title): + requirements = req + break + world.wait_for_requirejs(requirements) + + # 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. @@ -28,8 +69,6 @@ def wait(seconds): # 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): """ @@ -37,7 +76,7 @@ def wait_for_js_variable_truthy(variable): environment until the given variable is defined and truthy. This process guards against page reloads, and seamlessly retries on the next page. """ - js = """ + javascript = """ var callback = arguments[arguments.length - 1]; var unloadHandler = function() {{ callback("unload"); @@ -56,7 +95,13 @@ def wait_for_js_variable_truthy(variable): }}, 10); """.format(variable=variable) for _ in range(5): # 5 attempts max - result = world.browser.driver.execute_async_script(dedent(js)) + 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. @@ -105,7 +150,7 @@ def wait_for_requirejs(dependencies=None): if dependencies[0] != "jquery": dependencies.insert(0, "jquery") - js = """ + javascript = """ var callback = arguments[arguments.length - 1]; if(window.require) {{ requirejs.onError = callback; @@ -126,7 +171,13 @@ def wait_for_requirejs(dependencies=None): }} """.format(deps=json.dumps(dependencies)) for _ in range(5): # 5 attempts max - result = world.browser.driver.execute_async_script(dedent(js)) + 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. @@ -161,7 +212,7 @@ def wait_for_ajax_complete(): keeps track of this information, go here: http://stackoverflow.com/questions/3148225/jquery-active-function#3148506 """ - js = """ + javascript = """ var callback = arguments[arguments.length - 1]; if(!window.jQuery) {callback(false);} var intervalID = setInterval(function() { @@ -171,13 +222,13 @@ def wait_for_ajax_complete(): } }, 100); """ - world.browser.driver.execute_async_script(dedent(js)) + world.browser.driver.execute_async_script(dedent(javascript)) @world.absorb def visit(url): world.browser.visit(django_url(url)) - wait_for_requirejs() + wait_for_js_to_load() @world.absorb @@ -246,11 +297,11 @@ def css_has_value(css_selector, value, index=0): @world.absorb def wait_for(func, timeout=5): - WebDriverWait( - driver=world.browser.driver, - timeout=timeout, - ignored_exceptions=(StaleElementReferenceException) - ).until(func) + WebDriverWait( + driver=world.browser.driver, + timeout=timeout, + ignored_exceptions=(StaleElementReferenceException) + ).until(func) @world.absorb @@ -349,14 +400,10 @@ def css_click(css_selector, index=0, wait_time=30): msg="Element {}[{}] is present but not visible".format(css_selector, index) ) - # Sometimes you can't click in the center of the element, as - # another element might be on top of it. In this case, try - # clicking in the upper left corner. - try: - return retry_on_exception(lambda: world.css_find(css_selector)[index].click()) - - except WebDriverException: - return css_click_at(css_selector, index=index) + result = retry_on_exception(lambda: world.css_find(css_selector)[index].click()) + if result: + wait_for_js_to_load() + return result @world.absorb @@ -371,23 +418,6 @@ def css_check(css_selector, index=0, wait_time=30): return css_click(css_selector=css_selector, index=index, wait_time=wait_time) -@world.absorb -def css_click_at(css_selector, index=0, x_coord=10, y_coord=10, timeout=5): - ''' - A method to click at x,y coordinates of the element - rather than in the center of the element - ''' - wait_for_clickable(css_selector, timeout=timeout) - assert_true( - world.css_visible(css_selector, index=index), - msg="Element {}[{}] is present but not visible".format(css_selector, index) - ) - - element.action_chains.move_to_element_with_offset(element._element, x_coord, y_coord) - element.action_chains.click() - element.action_chains.perform() - - @world.absorb def select_option(name, value, index=0, wait_time=30): ''' @@ -417,6 +447,7 @@ def css_fill(css_selector, text, index=0): @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