Commit 33720a85 by Jay Zoldak

Merge pull request #1405 from edx/zoldak/fix-acceptance-fill-check

Add synchronization logic to fill, check, and select in acceptance tests.
parents a16c567b ecea49ba
......@@ -283,24 +283,33 @@ def button_disabled(step, value):
assert world.css_has_class(button_css, 'is-disabled')
@step('I confirm the prompt')
def confirm_the_prompt(step):
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))
def click_button(btn_css):
world.css_click(btn_css)
return world.css_find(btn_css).visible == False
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))
prompt_css = 'div.prompt.has-actions'
world.wait_for_visible(prompt_css)
btn_css = 'a.button.action-primary'
world.wait_for_visible(btn_css)
@world.absorb
def confirm_studio_prompt():
_do_studio_prompt_action('warning', 'primary')
# 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)
@step('I confirm the prompt')
def confirm_the_prompt(step):
confirm_studio_prompt()
@step(u'I am shown a prompt$')
......
......@@ -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):
......
......@@ -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
......
......@@ -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():
......
......@@ -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 "([^"]*)"$')
......
......@@ -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)
@world.absorb
def wait_for_present(css_selector, timeout=30):
def wait_for(func, timeout=5, timeout_msg=None):
"""
Waiting for the element to be present in the DOM.
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
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(EC.presence_of_element_located((By.CSS_SELECTOR, css_selector,)))
).until(func)
except TimeoutException:
raise TimeoutException("Timed out waiting for {} to be present.".format(css_selector))
raise TimeoutException(msg)
@world.absorb
def wait_for_visible(css_selector, timeout=30):
def wait_for_present(css_selector, 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 present 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 _: 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=30):
"""
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=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
......
......@@ -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)
......
......@@ -180,7 +180,6 @@ Feature: LMS.Answer problems
Given I am viewing a "<ProblemType>" problem
Then my "<ProblemType>" answer is marked "unanswered"
When I answer a "<ProblemType>" problem "<InitialCorrectness>ly"
And I wait for "1" seconds
And I input an answer on a "<ProblemType>" problem "<OtherCorrectness>ly"
Then my "<ProblemType>" 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 "<ProblemType>" problem
When I answer a "<ProblemType>" problem "<InitialCorrectness>ly"
And I wait for "1" seconds
Then my "<ProblemType>" answer is marked "<InitialCorrectness>"
And I input an answer on a "<ProblemType>" problem "<OtherCorrectness>ly"
Then my "<ProblemType>" answer is NOT marked "<InitialCorrectness>"
......
......@@ -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'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment