Commit cbbfc41c by Jay Zoldak

Merge pull request #995 from edx/zoldak/fix-acceptance

Simplify retry logic for ui helper functions
parents 259d3124 30b13d3c
...@@ -11,7 +11,6 @@ DISPLAY_NAME_KEY = "display_name" ...@@ -11,7 +11,6 @@ DISPLAY_NAME_KEY = "display_name"
DISPLAY_NAME_VALUE = '"Robot Super Course"' DISPLAY_NAME_VALUE = '"Robot Super Course"'
############### ACTIONS ####################
@step('I select the Advanced Settings$') @step('I select the Advanced Settings$')
def i_select_advanced_settings(step): def i_select_advanced_settings(step):
world.click_course_settings() world.click_course_settings()
...@@ -45,7 +44,6 @@ def create_value_not_in_quotes(step): ...@@ -45,7 +44,6 @@ def create_value_not_in_quotes(step):
change_display_name_value(step, 'quote me') change_display_name_value(step, 'quote me')
############### RESULTS ####################
@step('I see default advanced settings$') @step('I see default advanced settings$')
def i_see_default_advanced_settings(step): def i_see_default_advanced_settings(step):
# Test only a few of the existing properties (there are around 34 of them) # Test only a few of the existing properties (there are around 34 of them)
...@@ -88,12 +86,13 @@ def the_policy_key_value_is_changed(step): ...@@ -88,12 +86,13 @@ def the_policy_key_value_is_changed(step):
assert_equal(get_display_name_value(), '"foo"') assert_equal(get_display_name_value(), '"foo"')
############# HELPERS ###############
def assert_policy_entries(expected_keys, expected_values): def assert_policy_entries(expected_keys, expected_values):
for key, value in zip(expected_keys, expected_values): for key, value in zip(expected_keys, expected_values):
index = get_index_of(key) index = get_index_of(key)
assert_false(index == -1, "Could not find key: {key}".format(key=key)) assert_false(index == -1, "Could not find key: {key}".format(key=key))
assert_equal(value, world.css_find(VALUE_CSS)[index].value, "value is incorrect") found_value = world.css_find(VALUE_CSS)[index].value
assert_equal(value, found_value,
"Expected {} to have value {} but found {}".format(key, value, found_value))
def get_index_of(expected_key): def get_index_of(expected_key):
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
# pylint: disable=W0621 # pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from nose.tools import assert_true # 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 auth.authz import get_user_by_email, get_course_groupname_for_role
from django.conf import settings from django.conf import settings
...@@ -19,8 +19,6 @@ from terrain.browser import reset_data ...@@ -19,8 +19,6 @@ from terrain.browser import reset_data
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
########### STEP HELPERS ##############
@step('I (?:visit|access|open) the Studio homepage$') @step('I (?:visit|access|open) the Studio homepage$')
def i_visit_the_studio_homepage(_step): def i_visit_the_studio_homepage(_step):
...@@ -66,20 +64,32 @@ def select_new_course(_step, whom): ...@@ -66,20 +64,32 @@ def select_new_course(_step, whom):
@step(u'I press the "([^"]*)" notification button$') @step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(_step, name): def press_the_notification_button(_step, name):
css = 'a.action-%s' % name.lower() # TODO: fix up this code. Selenium is not dealing well with css transforms,
# as it thinks that the notification and the buttons are always visible
# The button was clicked if either the notification bar is gone,
# or we see an error overlaying it (expected for invalid inputs). # First wait for the notification to pop up
def button_clicked(): notification_css = 'div#page-notification div.wrapper-notification'
confirmation_dismissed = world.is_css_not_present('.is-shown.wrapper-notification-warning') world.wait_for_visible(notification_css)
error_showing = world.is_css_present('.is-shown.wrapper-notification-error')
return confirmation_dismissed or error_showing # You would think that the above would have worked, but it doesn't.
# Brute force wait for now.
world.wait(.5)
# Now make sure the button is there
btn_css = 'div#page-notification a.action-%s' % name.lower()
world.wait_for_visible(btn_css)
# You would think that the above would have worked, but it doesn't.
# Brute force wait for now.
world.wait(.5)
if world.is_firefox(): if world.is_firefox():
# This is done to explicitly make the changes save on firefox. It will remove focus from the previously focused element # This is done to explicitly make the changes save on firefox.
world.trigger_event(css, event='focus') # It will remove focus from the previously focused element
world.browser.execute_script("$('{}').click()".format(css)) world.trigger_event(btn_css, event='focus')
world.browser.execute_script("$('{}').click()".format(btn_css))
else: else:
world.css_click(css, success_condition=button_clicked), '%s button not clicked after 5 attempts.' % name world.css_click(btn_css)
@step('I change the "(.*)" field to "(.*)"$') @step('I change the "(.*)" field to "(.*)"$')
...@@ -110,7 +120,6 @@ def i_see_a_confirmation(step): ...@@ -110,7 +120,6 @@ def i_see_a_confirmation(step):
assert world.is_css_present(confirmation_css) assert world.is_css_present(confirmation_css)
####### HELPER FUNCTIONS ##############
def open_new_course(): def open_new_course():
world.clear_courses() world.clear_courses()
create_studio_user() create_studio_user()
...@@ -156,8 +165,8 @@ def log_into_studio( ...@@ -156,8 +165,8 @@ def log_into_studio(
world.log_in(username=uname, password=password, email=email, name=name) world.log_in(username=uname, password=password, email=email, name=name)
# Navigate to the studio dashboard # Navigate to the studio dashboard
world.visit('/') world.visit('/')
assert_in(uname, world.css_text('h2.title', timeout=10))
assert uname in world.css_text('h2.title', max_attempts=15)
def create_a_course(): def create_a_course():
course = world.CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') course = world.CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
...@@ -247,8 +256,22 @@ def button_disabled(step, value): ...@@ -247,8 +256,22 @@ def button_disabled(step, value):
@step('I confirm the prompt') @step('I confirm the prompt')
def confirm_the_prompt(step): def confirm_the_prompt(step):
prompt_css = 'a.button.action-primary'
world.css_click(prompt_css, success_condition=lambda: not world.css_visible(prompt_css)) 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)
@step(u'I am shown a (.*)$') @step(u'I am shown a (.*)$')
...@@ -257,6 +280,7 @@ def i_am_shown_a_notification(step, notification_type): ...@@ -257,6 +280,7 @@ def i_am_shown_a_notification(step, notification_type):
def type_in_codemirror(index, text): def type_in_codemirror(index, text):
world.wait(1) # For now, slow this down so that it works. TODO: fix it.
world.css_click("div.CodeMirror-lines", index=index) world.css_click("div.CodeMirror-lines", index=index)
world.browser.execute_script("$('div.CodeMirror.CodeMirror-focused > div').css('overflow', '')") world.browser.execute_script("$('div.CodeMirror.CodeMirror-focused > div').css('overflow', '')")
g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea") g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea")
......
...@@ -48,9 +48,7 @@ def click_component_from_menu(category, boilerplate, expected_css): ...@@ -48,9 +48,7 @@ def click_component_from_menu(category, boilerplate, expected_css):
elem_css = "a[data-category='{}']:not([data-boilerplate])".format(category) elem_css = "a[data-category='{}']:not([data-boilerplate])".format(category)
elements = world.css_find(elem_css) elements = world.css_find(elem_css)
assert_equal(len(elements), 1) assert_equal(len(elements), 1)
world.wait_for(lambda _driver: world.css_visible(elem_css)) world.css_click(elem_css)
world.css_click(elem_css, success_condition=lambda: 1 == len(world.css_find(expected_css)))
@world.absorb @world.absorb
def edit_component_and_select_settings(): def edit_component_and_select_settings():
......
...@@ -113,7 +113,7 @@ def test_i_have_entered_a_new_course_start_date(step): ...@@ -113,7 +113,7 @@ def test_i_have_entered_a_new_course_start_date(step):
@step('The warning about course start date goes away$') @step('The warning about course start date goes away$')
def test_the_warning_about_course_start_date_goes_away(step): def test_the_warning_about_course_start_date_goes_away(step):
assert_equal(0, len(world.css_find('.message-error'))) assert world.is_css_not_present('.message-error')
assert_false('error' in world.css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class')) assert_false('error' in world.css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class'))
assert_false('error' in world.css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class')) assert_false('error' in world.css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class'))
......
...@@ -5,7 +5,7 @@ from lettuce import world, step ...@@ -5,7 +5,7 @@ from lettuce import world, step
from common import create_studio_user from common import create_studio_user
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from auth.authz import get_course_groupname_for_role, get_user_by_email from auth.authz import get_course_groupname_for_role, get_user_by_email
from nose.tools import assert_true # pylint: disable=E0611 from nose.tools import assert_true, assert_in # pylint: disable=E0611
PASSWORD = 'test' PASSWORD = 'test'
EMAIL_EXTENSION = '@edx.org' EMAIL_EXTENSION = '@edx.org'
...@@ -110,36 +110,36 @@ def other_user_login(_step, name): ...@@ -110,36 +110,36 @@ def other_user_login(_step, name):
@step(u'I( do not)? see the course on my 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') @step(u's?he does( not)? see the course on (his|her) page')
def see_course(_step, inverted, gender='self'): def see_course(_step, do_not_see, gender='self'):
class_css = 'h3.course-title' class_css = 'h3.course-title'
all_courses = world.css_find(class_css, wait_time=1) if do_not_see:
all_names = [item.html for item in all_courses] assert world.is_css_not_present(class_css)
if inverted:
assert not world.scenario_dict['COURSE'].display_name in all_names
else: else:
assert world.scenario_dict['COURSE'].display_name in all_names all_courses = world.css_find(class_css)
all_names = [item.html for item in all_courses]
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, inverted): def marked_as_admin(_step, name, not_marked_admin):
flag_css = '.user-item[data-email="{email}"] .flag-role.flag-role-admin'.format( flag_css = '.user-item[data-email="{email}"] .flag-role.flag-role-admin'.format(
email=name+EMAIL_EXTENSION) email=name+EMAIL_EXTENSION)
if inverted: if not_marked_admin:
assert world.is_css_not_present(flag_css) assert world.is_css_not_present(flag_css)
else: else:
assert world.is_css_present(flag_css) 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, inverted): def self_marked_as_admin(_step, not_marked_admin):
return marked_as_admin(_step, "robot+studio", inverted) return marked_as_admin(_step, "robot+studio", not_marked_admin)
@step(u'I can(not)? delete users') @step(u'I can(not)? delete users')
@step(u's?he can(not)? delete users') @step(u's?he can(not)? delete users')
def can_delete_users(_step, inverted): def can_delete_users(_step, can_not_delete):
to_delete_css = 'a.remove-user' to_delete_css = 'a.remove-user'
if inverted: if can_not_delete:
assert world.is_css_not_present(to_delete_css) assert world.is_css_not_present(to_delete_css)
else: else:
assert world.is_css_present(to_delete_css) assert world.is_css_present(to_delete_css)
...@@ -147,9 +147,9 @@ def can_delete_users(_step, inverted): ...@@ -147,9 +147,9 @@ def can_delete_users(_step, inverted):
@step(u'I can(not)? add users') @step(u'I can(not)? add users')
@step(u's?he can(not)? add users') @step(u's?he can(not)? add users')
def can_add_users(_step, inverted): def can_add_users(_step, can_not_add):
add_css = 'a.create-user-button' add_css = 'a.create-user-button'
if inverted: if can_not_add:
assert world.is_css_not_present(add_css) assert world.is_css_not_present(add_css)
else: else:
assert world.is_css_present(add_css) assert world.is_css_present(add_css)
...@@ -157,13 +157,13 @@ def can_add_users(_step, inverted): ...@@ -157,13 +157,13 @@ def can_add_users(_step, inverted):
@step(u'I can(not)? make ("([^"]*)"|myself) 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') @step(u's?he can(not)? make ("([^"]*)"|me) a course team admin')
def can_make_course_admin(_step, inverted, outer_capture, name): def can_make_course_admin(_step, can_not_make_admin, outer_capture, name):
if outer_capture == "myself": if outer_capture == "myself":
email = world.scenario_dict["USER"].email email = world.scenario_dict["USER"].email
else: else:
email = name + EMAIL_EXTENSION email = name + EMAIL_EXTENSION
add_button_css = '.user-item[data-email="{email}"] .add-admin-role'.format(email=email) add_button_css = '.user-item[data-email="{email}"] .add-admin-role'.format(email=email)
if inverted: if can_not_make_admin:
assert world.is_css_not_present(add_button_css) assert world.is_css_not_present(add_button_css)
else: else:
assert world.is_css_present(add_button_css) assert world.is_css_present(add_button_css)
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
from lettuce import world, step from lettuce import world, step
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
from common import type_in_codemirror from common import type_in_codemirror
from nose.tools import assert_in # pylint: disable=E0611
@step(u'I go to the course updates page') @step(u'I go to the course updates page')
...@@ -21,14 +22,17 @@ def add_update(_step, text): ...@@ -21,14 +22,17 @@ def add_update(_step, text):
change_text(text) change_text(text)
@step(u'I should( not)? see the update "([^"]*)"$') @step(u'I should see the update "([^"]*)"$')
def check_update(_step, doesnt_see_update, text): def check_update(_step, text):
update_css = 'div.update-contents' update_css = 'div.update-contents'
update = world.css_find(update_css, wait_time=1) update_html = world.css_find(update_css).html
if doesnt_see_update: assert_in(text, update_html)
assert len(update) == 0 or not text in update.html
else:
assert text in update.html @step(u'I should not see the update "([^"]*)"$')
def check_no_update(_step, text):
update_css = 'div.update-contents'
assert world.is_css_not_present(update_css)
@step(u'I modify the text to "([^"]*)"$') @step(u'I modify the text to "([^"]*)"$')
......
...@@ -5,6 +5,7 @@ from lettuce import world, step ...@@ -5,6 +5,7 @@ from lettuce import world, step
from common import * from common import *
from terrain.steps import reload_the_page from terrain.steps import reload_the_page
from selenium.common.exceptions import InvalidElementStateException from selenium.common.exceptions import InvalidElementStateException
from nose.tools import assert_in, assert_not_in # pylint: disable=E0611
@step(u'I am viewing the grading settings') @step(u'I am viewing the grading settings')
...@@ -65,21 +66,25 @@ def change_assignment_name(step, old_name, new_name): ...@@ -65,21 +66,25 @@ def change_assignment_name(step, old_name, new_name):
@step(u'I go back to the main course page') @step(u'I go back to the main course page')
def main_course_page(step): def main_course_page(step):
main_page_link_css = 'a[href="/%s/%s/course/%s"]' % (world.scenario_dict['COURSE'].org, main_page_link = '/{}/{}/course/{}'.format(world.scenario_dict['COURSE'].org,
world.scenario_dict['COURSE'].number, world.scenario_dict['COURSE'].number,
world.scenario_dict['COURSE'].display_name.replace(' ', '_'),) world.scenario_dict['COURSE'].display_name.replace(' ', '_'),)
world.css_click(main_page_link_css) world.visit(main_page_link)
assert_in('Course Outline', world.css_text('h1.page-header'))
@step(u'I do( not)? see the assignment name "([^"]*)"$') @step(u'I do( not)? see the assignment name "([^"]*)"$')
def see_assignment_name(step, do_not, name): def see_assignment_name(step, do_not, name):
assignment_menu_css = 'ul.menu > li > a' assignment_menu_css = 'ul.menu > li > a'
# First assert that it is there, make take a bit to redraw
assert world.css_find(assignment_menu_css)
assignment_menu = world.css_find(assignment_menu_css) assignment_menu = world.css_find(assignment_menu_css)
allnames = [item.html for item in assignment_menu] allnames = [item.html for item in assignment_menu]
if do_not: if do_not:
assert not name in allnames assert_not_in(name, allnames)
else: else:
assert name in allnames assert_in(name, allnames)
@step(u'I delete the assignment type "([^"]*)"$') @step(u'I delete the assignment type "([^"]*)"$')
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
#pylint: disable=C0111 #pylint: disable=C0111
from lettuce import world, step from lettuce import world, step
from nose.tools import assert_equal # pylint: disable=E0611 from nose.tools import assert_equal, assert_true # pylint: disable=E0611
from common import type_in_codemirror from common import type_in_codemirror
DISPLAY_NAME = "Display Name" DISPLAY_NAME = "Display Name"
...@@ -197,9 +197,20 @@ def high_level_source_in_editor(step): ...@@ -197,9 +197,20 @@ def high_level_source_in_editor(step):
def verify_high_level_source_links(step, visible): def verify_high_level_source_links(step, visible):
assert_equal(visible, world.is_css_present('.launch-latex-compiler')) 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) world.cancel_component(step)
assert_equal(visible, world.is_css_present('.upload-button')) if visible:
assert_true(world.is_css_present('.upload-button'),
msg="Expected to find the upload button but it is not present.")
else:
assert_true(world.is_css_not_present('.upload-button'),
msg="Expected not to find the upload button but it is present.")
def verify_modified_weight(): def verify_modified_weight():
......
...@@ -12,7 +12,7 @@ def i_fill_in_the_registration_form(step): ...@@ -12,7 +12,7 @@ def i_fill_in_the_registration_form(step):
register_form.find_by_name('password').fill('test') register_form.find_by_name('password').fill('test')
register_form.find_by_name('username').fill('robot-studio') register_form.find_by_name('username').fill('robot-studio')
register_form.find_by_name('name').fill('Robot Studio') register_form.find_by_name('name').fill('Robot Studio')
register_form.find_by_name('terms_of_service').check() register_form.find_by_name('terms_of_service').click()
world.retry_on_exception(fill_in_reg_form) world.retry_on_exception(fill_in_reg_form)
......
...@@ -2,12 +2,11 @@ ...@@ -2,12 +2,11 @@
#pylint: disable=W0621 #pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from selenium.webdriver.common.keys import Keys
from nose.tools import assert_true # pylint: disable=E0611 from nose.tools import assert_true # pylint: disable=E0611
@step(u'I go to the static pages page') @step(u'I go to the static pages page')
def go_to_static(_step): def go_to_static(step):
menu_css = 'li.nav-course-courseware' menu_css = 'li.nav-course-courseware'
static_css = 'li.nav-course-courseware-pages a' static_css = 'li.nav-course-courseware-pages a'
world.css_click(menu_css) world.css_click(menu_css)
...@@ -15,25 +14,37 @@ def go_to_static(_step): ...@@ -15,25 +14,37 @@ def go_to_static(_step):
@step(u'I add a new page') @step(u'I add a new page')
def add_page(_step): def add_page(step):
button_css = 'a.new-button' button_css = 'a.new-button'
world.css_click(button_css) world.css_click(button_css)
@step(u'I should( not)? see a "([^"]*)" static page$') @step(u'I should not see a "([^"]*)" static page$')
def see_page(_step, doesnt, page): def not_see_page(step, page):
# Either there are no pages, or there are pages but
# not the one I expect not to exist.
should_exist = not doesnt # Since our only test for deletion right now deletes
# the only static page that existed, our success criteria
# will be that there are no static pages.
# In the future we can refactor if necessary.
tabs_css = 'li.component'
assert (world.is_css_not_present(tabs_css, wait_time=30))
@step(u'I should see a "([^"]*)" static page$')
def see_page(step, page):
# Need to retry here because the element # Need to retry here because the element
# will sometimes exist before the HTML content is loaded # will sometimes exist before the HTML content is loaded
exists_func = lambda(driver): page_exists(page) == should_exist exists_func = lambda(driver): page_exists(page)
world.wait_for(exists_func) world.wait_for(exists_func)
assert_true(exists_func(None)) assert_true(exists_func(None))
@step(u'I "([^"]*)" the "([^"]*)" page$') @step(u'I "([^"]*)" the "([^"]*)" page$')
def click_edit_delete(_step, edit_delete, page): def click_edit_delete(step, edit_delete, page):
button_css = 'a.%s-button' % edit_delete button_css = 'a.%s-button' % edit_delete
index = get_index(page) index = get_index(page)
assert index is not None assert index is not None
...@@ -41,7 +52,7 @@ def click_edit_delete(_step, edit_delete, page): ...@@ -41,7 +52,7 @@ def click_edit_delete(_step, edit_delete, page):
@step(u'I change the name to "([^"]*)"$') @step(u'I change the name to "([^"]*)"$')
def change_name(_step, new_name): def change_name(step, new_name):
settings_css = '#settings-mode a' settings_css = '#settings-mode a'
world.css_click(settings_css) world.css_click(settings_css)
input_css = 'input.setting-input' input_css = 'input.setting-input'
...@@ -56,9 +67,10 @@ def get_index(name): ...@@ -56,9 +67,10 @@ def get_index(name):
page_name_css = 'section[data-type="HTMLModule"]' page_name_css = 'section[data-type="HTMLModule"]'
all_pages = world.css_find(page_name_css) all_pages = world.css_find(page_name_css)
for i in range(len(all_pages)): for i in range(len(all_pages)):
if world.css_html(page_name_css, index=i) == '\n {name}\n'.format(name=name): if all_pages[i].html == '\n {name}\n'.format(name=name):
return i return i
return None return None
def page_exists(page): def page_exists(page):
return get_index(page) is not None return get_index(page) is not None
...@@ -7,6 +7,8 @@ import requests ...@@ -7,6 +7,8 @@ import requests
import string import string
import random import random
import os import os
from nose.tools import assert_equal, assert_not_equal # pylint: disable=E0611
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
...@@ -32,7 +34,7 @@ def upload_file(_step, file_name): ...@@ -32,7 +34,7 @@ def upload_file(_step, file_name):
@step(u'I upload the files (".*")$') @step(u'I upload the files (".*")$')
def upload_file(_step, files_string): def upload_files(_step, files_string):
# Turn files_string to a list of file names # Turn files_string to a list of file names
files = files_string.split(",") files = files_string.split(",")
files = map(lambda x: string.strip(x, ' "\''), files) files = map(lambda x: string.strip(x, ' "\''), files)
...@@ -48,19 +50,29 @@ def upload_file(_step, files_string): ...@@ -48,19 +50,29 @@ def upload_file(_step, files_string):
world.css_click(close_css) world.css_click(close_css)
@step(u'I should( not)? see the file "([^"]*)" was uploaded$') @step(u'I should not see the file "([^"]*)" was uploaded$')
def check_upload(_step, do_not_see_file, file_name): def check_not_there(_step, file_name):
# Either there are no files, or there are files but
# not the one I expect not to exist.
# Since our only test for deletion right now deletes
# the only file that was uploaded, our success criteria
# will be that there are no files.
# In the future we can refactor if necessary.
names_css = 'td.name-col > a.filename'
assert(world.is_css_not_present(names_css))
@step(u'I should see the file "([^"]*)" was uploaded$')
def check_upload(_step, file_name):
index = get_index(file_name) index = get_index(file_name)
if do_not_see_file: assert_not_equal(index, -1)
assert index == -1
else:
assert index != -1
@step(u'The url for the file "([^"]*)" is valid$') @step(u'The url for the file "([^"]*)" is valid$')
def check_url(_step, file_name): def check_url(_step, file_name):
r = get_file(file_name) r = get_file(file_name)
assert r.status_code == 200 assert_equal(r.status_code , 200)
@step(u'I delete the file "([^"]*)"$') @step(u'I delete the file "([^"]*)"$')
...@@ -71,7 +83,7 @@ def delete_file(_step, file_name): ...@@ -71,7 +83,7 @@ def delete_file(_step, file_name):
world.css_click(delete_css, index=index) world.css_click(delete_css, index=index)
prompt_confirm_css = 'li.nav-item > a.action-primary' prompt_confirm_css = 'li.nav-item > a.action-primary'
world.css_click(prompt_confirm_css, success_condition=lambda: not world.css_visible(prompt_confirm_css)) world.css_click(prompt_confirm_css)
@step(u'I should see only one "([^"]*)"$') @step(u'I should see only one "([^"]*)"$')
......
...@@ -75,7 +75,8 @@ def make_desired_capabilities(): ...@@ -75,7 +75,8 @@ def make_desired_capabilities():
desired_capabilities['build'] = settings.SAUCE.get('BUILD') desired_capabilities['build'] = settings.SAUCE.get('BUILD')
desired_capabilities['video-upload-on-pass'] = False desired_capabilities['video-upload-on-pass'] = False
desired_capabilities['sauce-advisor'] = False desired_capabilities['sauce-advisor'] = False
desired_capabilities['record-screenshots'] = False desired_capabilities['capture-html'] = True
desired_capabilities['record-screenshots'] = True
desired_capabilities['selenium-version'] = "2.34.0" desired_capabilities['selenium-version'] = "2.34.0"
desired_capabilities['max-duration'] = 3600 desired_capabilities['max-duration'] = 3600
desired_capabilities['public'] = 'public restricted' desired_capabilities['public'] = 'public restricted'
...@@ -164,15 +165,18 @@ def reset_databases(scenario): ...@@ -164,15 +165,18 @@ def reset_databases(scenario):
xmodule.modulestore.django.clear_existing_modulestores() xmodule.modulestore.django.clear_existing_modulestores()
# Uncomment below to trigger a screenshot on error @after.each_scenario
# @after.each_scenario
def screenshot_on_error(scenario): def screenshot_on_error(scenario):
""" """
Save a screenshot to help with debugging. Save a screenshot to help with debugging.
""" """
if scenario.failed: if scenario.failed:
world.browser.driver.save_screenshot('/tmp/last_failed_scenario.png') 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.all @after.all
def teardown_browser(total): def teardown_browser(total):
......
...@@ -5,7 +5,7 @@ from lettuce import world ...@@ -5,7 +5,7 @@ from lettuce import world
import time import time
import platform import platform
from urllib import quote_plus from urllib import quote_plus
from selenium.common.exceptions import WebDriverException, StaleElementReferenceException from selenium.common.exceptions import WebDriverException, TimeoutException
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
...@@ -19,11 +19,6 @@ def wait(seconds): ...@@ -19,11 +19,6 @@ def wait(seconds):
@world.absorb @world.absorb
def wait_for(func):
WebDriverWait(world.browser.driver, 5).until(func)
@world.absorb
def visit(url): def visit(url):
world.browser.visit(django_url(url)) world.browser.visit(django_url(url))
...@@ -34,7 +29,7 @@ def url_equals(url): ...@@ -34,7 +29,7 @@ def url_equals(url):
@world.absorb @world.absorb
def is_css_present(css_selector, wait_time=5): def is_css_present(css_selector, wait_time=10):
return world.browser.is_element_present_by_css(css_selector, wait_time=wait_time) return world.browser.is_element_present_by_css(css_selector, wait_time=wait_time)
...@@ -44,92 +39,130 @@ def is_css_not_present(css_selector, wait_time=5): ...@@ -44,92 +39,130 @@ def is_css_not_present(css_selector, wait_time=5):
@world.absorb @world.absorb
def css_has_text(css_selector, text, index=0, max_attempts=5): def css_has_text(css_selector, text, index=0):
return world.css_text(css_selector, index=index, max_attempts=max_attempts) == text return world.css_text(css_selector, index=index) == text
@world.absorb @world.absorb
def css_find(css, wait_time=5): def wait_for(func, timeout=5):
def is_visible(_driver): WebDriverWait(world.browser.driver, timeout).until(func)
return EC.visibility_of_element_located((By.CSS_SELECTOR, css,))
world.browser.is_element_present_by_css(css, wait_time=wait_time)
wait_for(is_visible) def wait_for_present(css_selector, timeout=30):
return world.browser.find_by_css(css) """
Waiting for the element to be present in the DOM.
Throws an error if the wait_for time expires.
Otherwise this method will return None
"""
try:
WebDriverWait(driver=world.browser.driver,
timeout=60).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))
@world.absorb @world.absorb
def css_click(css_selector, index=0, max_attempts=5, success_condition=lambda: True): def wait_for_visible(css_selector, timeout=30):
"""
Waiting for the element to be visible in the DOM.
Throws an error if the wait_for time expires.
Otherwise this method will return None
""" """
Perform a click on a CSS selector, retrying if it initially fails. try:
WebDriverWait(driver=world.browser.driver,
timeout=timeout).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))
This function handles errors that may be thrown if the component cannot be clicked on.
However, there are cases where an error may not be thrown, and yet the operation did not
actually succeed. For those cases, a success_condition lambda can be supplied to verify that the click worked.
This function will return True if the click worked (taking into account both errors and the optional @world.absorb
success_condition). def wait_for_invisible(css_selector, timeout=30):
""" """
assert is_css_present(css_selector), "{} is not present".format(css_selector) Waiting for the element to be either invisible or not present on the DOM.
for _ in range(max_attempts): Throws an error if the wait_for time expires.
try: Otherwise this method will return None
world.css_find(css_selector)[index].click() """
if success_condition(): try:
return WebDriverWait(driver=world.browser.driver,
except WebDriverException: timeout=timeout).until(EC.invisibility_of_element_located((By.CSS_SELECTOR, css_selector,)))
# Occasionally, MathJax or other JavaScript can cover up except TimeoutException:
# an element temporarily. raise TimeoutException("Timed out waiting for {} to be invisible.".format(css_selector))
# If this happens, wait a second, then try again
world.wait(1)
except:
pass
else:
# try once more, letting exceptions raise
world.css_find(css_selector)[index].click()
if not success_condition():
raise Exception("unsuccessful click")
@world.absorb @world.absorb
def css_check(css_selector, index=0, max_attempts=5, success_condition=lambda: True): def wait_for_clickable(css_selector, timeout=30):
""" """
Checks a check box based on a CSS selector, retrying if it initially fails. Waiting for the element to be present and clickable.
Throws an error if the wait_for time expires.
Otherwise this method will return None.
"""
# Sometimes the element is clickable then gets obscured.
# In this case, pause so that it is not reported clickable too early
try:
WebDriverWait(world.browser.driver,
timeout=timeout).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))
This function handles errors that may be thrown if the component cannot be clicked on.
However, there are cases where an error may not be thrown, and yet the operation did not
actually succeed. For those cases, a success_condition lambda can be supplied to verify that the check worked.
This function will return True if the check worked (taking into account both errors and the optional @world.absorb
success_condition). def css_find(css, wait_time=30):
""" """
assert is_css_present(css_selector), "{} is not present".format(css_selector) Wait for the element(s) as defined by css locator
for _ in range(max_attempts): to be present.
try:
world.css_find(css_selector)[index].check() This method will return a WebDriverElement.
if success_condition(): """
return wait_for_present(css_selector=css, timeout=wait_time)
except WebDriverException: return world.browser.find_by_css(css)
# Occasionally, MathJax or other JavaScript can cover up
# an element temporarily.
# If this happens, wait a second, then try again @world.absorb
world.wait(1) def css_click(css_selector, index=0, wait_time=30):
except: """
pass Perform a click on a CSS selector, first waiting for the element
else: to be present and clickable.
# try once more, letting exceptions raise
world.css_find(css_selector)[index].check() This method will return True if the click worked.
if not success_condition(): """
raise Exception("unsuccessful check") wait_for_clickable(css_selector, timeout=wait_time)
assert_true(world.css_find(css_selector)[index].visible,
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 world.css_find(css_selector)[index].click()
except WebDriverException:
return css_click_at(css_selector, index=index)
@world.absorb @world.absorb
def css_click_at(css, x_cord=10, y_cord=10): def css_check(css_selector, index=0, 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.
This method will return True if the check worked.
"""
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 A method to click at x,y coordinates of the element
rather than in the center of the element rather than in the center of the element
''' '''
element = css_find(css).first wait_for_clickable(css_selector, timeout=timeout)
element.action_chains.move_to_element_with_offset(element._element, x_cord, y_cord) element = css_find(css_selector)[index]
assert_true(element.visible,
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.click()
element.action_chains.perform() element.action_chains.perform()
...@@ -139,58 +172,55 @@ def id_click(elem_id): ...@@ -139,58 +172,55 @@ def id_click(elem_id):
""" """
Perform a click on an element as specified by its id Perform a click on an element as specified by its id
""" """
world.css_click('#%s' % elem_id) css_click('#{}'.format(elem_id))
@world.absorb @world.absorb
def css_fill(css_selector, text, index=0, max_attempts=5): def css_fill(css_selector, text, index=0):
assert is_css_present(css_selector) css_find(css_selector)[index].fill(text)
return world.retry_on_exception(lambda: world.browser.find_by_css(css_selector)[index].fill(text), max_attempts=max_attempts)
@world.absorb @world.absorb
def click_link(partial_text, index=0, max_attempts=5): def click_link(partial_text, index=0):
return world.retry_on_exception(lambda: world.browser.find_link_by_partial_text(partial_text)[index].click(), max_attempts=max_attempts) world.browser.find_link_by_partial_text(partial_text)[index].click()
@world.absorb @world.absorb
def css_text(css_selector, index=0, max_attempts=5): def css_text(css_selector, index=0, timeout=30):
# Wait for the css selector to appear # Wait for the css selector to appear
if world.is_css_present(css_selector): if is_css_present(css_selector):
return world.retry_on_exception(lambda: world.browser.find_by_css(css_selector)[index].text, max_attempts=max_attempts) return css_find(css_selector, wait_time=timeout)[index].text
else: else:
return "" return ""
@world.absorb @world.absorb
def css_value(css_selector, index=0, max_attempts=5): def css_value(css_selector, index=0):
# Wait for the css selector to appear # Wait for the css selector to appear
if world.is_css_present(css_selector): if is_css_present(css_selector):
return world.retry_on_exception(lambda: world.browser.find_by_css(css_selector)[index].value, max_attempts=max_attempts) return css_find(css_selector)[index].value
else: else:
return "" return ""
@world.absorb @world.absorb
def css_html(css_selector, index=0, max_attempts=5): def css_html(css_selector, index=0):
""" """
Returns the HTML of a css_selector and will retry if there is a StaleElementReferenceException Returns the HTML of a css_selector
""" """
assert is_css_present(css_selector) assert is_css_present(css_selector)
return world.retry_on_exception(lambda: world.browser.find_by_css(css_selector)[index].html, max_attempts=max_attempts) return css_find(css_selector)[index].html
@world.absorb @world.absorb
def css_has_class(css_selector, class_name, index=0, max_attempts=5): def css_has_class(css_selector, class_name, index=0):
return world.retry_on_exception(lambda: world.css_find(css_selector)[index].has_class(class_name), max_attempts=max_attempts) return css_find(css_selector)[index].has_class(class_name)
@world.absorb @world.absorb
def css_visible(css_selector, index=0, max_attempts=5): def css_visible(css_selector, index=0):
assert is_css_present(css_selector) assert is_css_present(css_selector)
return world.retry_on_exception(lambda: world.browser.find_by_css(css_selector)[index].visible, max_attempts=max_attempts) return css_find(css_selector)[index].visible
@world.absorb @world.absorb
...@@ -235,14 +265,17 @@ def click_tools(): ...@@ -235,14 +265,17 @@ def click_tools():
def is_mac(): def is_mac():
return platform.mac_ver()[0] is not '' return platform.mac_ver()[0] is not ''
@world.absorb @world.absorb
def is_firefox(): def is_firefox():
return world.browser.driver_name is 'Firefox' return world.browser.driver_name is 'Firefox'
@world.absorb @world.absorb
def trigger_event(css_selector, event='change', index=0): def trigger_event(css_selector, event='change', index=0):
world.browser.execute_script("$('{}:eq({})').trigger('{}')".format(css_selector, index, event)) world.browser.execute_script("$('{}:eq({})').trigger('{}')".format(css_selector, index, event))
@world.absorb @world.absorb
def retry_on_exception(func, max_attempts=5): def retry_on_exception(func, max_attempts=5):
attempt = 0 attempt = 0
......
...@@ -80,13 +80,9 @@ def click_on_section(step, section): ...@@ -80,13 +80,9 @@ def click_on_section(step, section):
section_css = 'h3[tabindex="-1"]' section_css = 'h3[tabindex="-1"]'
world.css_click(section_css) world.css_click(section_css)
subid = "ui-accordion-accordion-panel-" + str(int(section) - 1) subid = "ui-accordion-accordion-panel-{}".format(str(int(section) - 1))
subsection_css = 'ul.ui-accordion-content-active[id=\'%s\'] > li > a' % subid subsection_css = "ul.ui-accordion-content-active[id='{}'] > li > a".format(subid)
prev_url = world.browser.url world.css_click(subsection_css)
changed_section = lambda: prev_url != world.browser.url
#for some reason needed to do it in two steps
world.css_click(subsection_css, success_condition=changed_section)
@step(u'I click on subsection "([^"]*)"$') @step(u'I click on subsection "([^"]*)"$')
......
...@@ -129,7 +129,7 @@ Feature: Answer problems ...@@ -129,7 +129,7 @@ Feature: Answer problems
When I press the button with the label "Hide Answer(s)" When I press the button with the label "Hide Answer(s)"
Then the button with the label "Show Answer(s)" does appear Then the button with the label "Show Answer(s)" does appear
And I should not see "4.14159" anywhere on the page And I should not see "4.14159" anywhere on the page
Scenario: I can see my score on a problem when I answer it and after I reset it Scenario: I can see my score on a problem when I answer it and after I reset it
Given I am viewing a "<ProblemType>" problem Given I am viewing a "<ProblemType>" problem
When I answer a "<ProblemType>" problem "<Correctness>ly" When I answer a "<ProblemType>" problem "<Correctness>ly"
......
...@@ -101,6 +101,9 @@ def input_problem_answer(_, problem_type, correctness): ...@@ -101,6 +101,9 @@ def input_problem_answer(_, problem_type, correctness):
@step(u'I check a problem') @step(u'I check a problem')
def check_problem(step): def check_problem(step):
# first scroll down so the loading mathjax button does not
# cover up the Check button
world.browser.execute_script("window.scrollTo(0,1024)")
world.css_click("input.check") world.css_click("input.check")
......
...@@ -166,6 +166,8 @@ def answer_problem(problem_type, correctness): ...@@ -166,6 +166,8 @@ def answer_problem(problem_type, correctness):
if problem_type == "drop down": if problem_type == "drop down":
select_name = "input_i4x-edx-model_course-problem-drop_down_2_1" select_name = "input_i4x-edx-model_course-problem-drop_down_2_1"
option_text = 'Option 2' if correctness == 'correct' else 'Option 3' option_text = 'Option 2' if correctness == 'correct' else 'Option 3'
# First wait for the element to be there on the page
world.wait_for_visible("select#{}".format(select_name))
world.browser.select(select_name, option_text) world.browser.select(select_name, option_text)
elif problem_type == "multiple choice": elif problem_type == "multiple choice":
......
...@@ -23,9 +23,8 @@ def i_press_the_button_on_the_registration_form(step): ...@@ -23,9 +23,8 @@ def i_press_the_button_on_the_registration_form(step):
@step('I check the checkbox named "([^"]*)"$') @step('I check the checkbox named "([^"]*)"$')
def i_check_checkbox(step, checkbox): def i_check_checkbox(step, checkbox):
def check_box(): css_selector = 'input[name={}]'.format(checkbox)
world.browser.find_by_name(checkbox).check() world.css_check(css_selector)
world.retry_on_exception(check_box)
@step('I should see "([^"]*)" in the dashboard banner$') @step('I should see "([^"]*)" in the dashboard banner$')
......
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