Files
edx-platform/common/djangoapps/terrain/ui_helpers.py
Will Daly 9ab35759be Added wait for problem to re-render when checking or resetting it
Added wait for JS on upload page
Fixed an issue where css_click wasn't waiting for JS to load
2013-11-01 14:31:37 -04:00

612 lines
19 KiB
Python

#pylint: disable=C0111
#pylint: disable=W0621
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)
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
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/base", "js/models/course",
"js/models/settings/course_details", "js/views/settings/main"],
# Settings - Advanced Settings
re.compile('^Advanced Settings \|'): [
"jquery", "js/base", "js/models/course", "js/models/settings/advanced",
"js/views/settings/advanced", "codemirror"],
# Individual Unit (editing)
re.compile('^Individual Unit \|'): [
"js/base", "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/base", "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"],
# Upload
re.compile(r'^\s*Files & Uploads'): [
'js/base', 'jquery.ui', 'coffee/src/main', 'underscore',
'js/views/assets', 'js/views/asset'
]
}
@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.
# This callback is passed as the last argument to the script. Any arguments
# passed to this callback get returned from the `execute_async_script`
# function, which allows the JS to communicate information back to Python.
# Ref: https://selenium.googlecode.com/svn/trunk/docs/api/dotnet/html/M_OpenQA_Selenium_IJavaScriptExecutor_ExecuteAsyncScript.htm
@world.absorb
def wait_for_js_variable_truthy(variable):
"""
Using Selenium's `execute_async_script` function, poll the Javascript
environment until the given variable is defined and truthy. This process
guards against page reloads, and seamlessly retries on the next page.
"""
javascript = """
var callback = arguments[arguments.length - 1];
var unloadHandler = function() {{
callback("unload");
}}
addEventListener("beforeunload", unloadHandler);
addEventListener("unload", unloadHandler);
var intervalID = setInterval(function() {{
try {{
if({variable}) {{
clearInterval(intervalID);
removeEventListener("beforeunload", unloadHandler);
removeEventListener("unload", unloadHandler);
callback(true);
}}
}} catch (e) {{}}
}}, 10);
""".format(variable=variable)
for _ in range(5): # 5 attempts max
try:
result = world.browser.driver.execute_async_script(dedent(javascript))
except WebDriverException as wde:
if "document unloaded while waiting for result" in wde.msg:
result = "unload"
else:
raise
if result == "unload":
# we ran this on the wrong page. Wait a bit, and try again, when the
# browser has loaded the next page.
world.wait(1)
continue
else:
return result
@world.absorb
def wait_for_xmodule():
"Wait until the XModule Javascript has loaded on the page."
world.wait_for_js_variable_truthy("XModule")
world.wait_for_js_variable_truthy("XBlock")
@world.absorb
def wait_for_mathjax():
"Wait until MathJax is loaded and set up on the page."
world.wait_for_js_variable_truthy("MathJax.isReady")
class RequireJSError(Exception):
"""
An error related to waiting for require.js. If require.js is unable to load
a dependency in the `wait_for_requirejs` function, Python will throw
this exception to make sure that the failure doesn't pass silently.
"""
pass
@world.absorb
def wait_for_requirejs(dependencies=None):
"""
If requirejs is loaded on the page, this function will pause
Selenium until require is finished loading the given dependencies.
If requirejs is not loaded on the page, this function will return
immediately.
:param dependencies: a list of strings that identify resources that
we should wait for requirejs to load. By default, requirejs will only
wait for jquery.
"""
if not dependencies:
dependencies = ["jquery"]
# stick jquery at the front
if dependencies[0] != "jquery":
dependencies.insert(0, "jquery")
javascript = """
var callback = arguments[arguments.length - 1];
if(window.require) {{
requirejs.onError = callback;
var unloadHandler = function() {{
callback("unload");
}}
addEventListener("beforeunload", unloadHandler);
addEventListener("unload", unloadHandler);
require({deps}, function($) {{
setTimeout(function() {{
removeEventListener("beforeunload", unloadHandler);
removeEventListener("unload", unloadHandler);
callback(true);
}}, 50);
}});
}} else {{
callback(false);
}}
""".format(deps=json.dumps(dependencies))
for _ in range(5): # 5 attempts max
try:
result = world.browser.driver.execute_async_script(dedent(javascript))
except WebDriverException as wde:
if "document unloaded while waiting for result" in wde.msg:
result = "unload"
else:
raise
if result == "unload":
# we ran this on the wrong page. Wait a bit, and try again, when the
# browser has loaded the next page.
world.wait(1)
continue
elif result not in (None, True, False):
# We got a require.js error
# Sometimes requireJS will throw an error with requireType=require
# This doesn't seem to cause problems on the page, so we ignore it
if result['requireType'] == 'require':
world.wait(1)
continue
# Otherwise, fail and report the error
else:
msg = "Error loading dependencies: type={0} modules={1}".format(
result['requireType'], result['requireModules'])
err = RequireJSError(msg)
err.error = result
raise err
else:
return result
@world.absorb
def wait_for_ajax_complete():
"""
Wait until all jQuery AJAX calls have completed. "Complete" means that
either the server has sent a response (regardless of whether the response
indicates success or failure), or that the AJAX call timed out waiting for
a response. For more information about the `jQuery.active` counter that
keeps track of this information, go here:
http://stackoverflow.com/questions/3148225/jquery-active-function#3148506
"""
javascript = """
var callback = arguments[arguments.length - 1];
if(!window.jQuery) {callback(false);}
var intervalID = setInterval(function() {
if(jQuery.active == 0) {
clearInterval(intervalID);
callback(true);
}
}, 100);
"""
# Sometimes the ajax when it returns will make the browser reload
# the DOM, and throw a WebDriverException with the message:
# 'javascript error: document unloaded while waiting for result'
for _ in range(5): # 5 attempts max
try:
result = world.browser.driver.execute_async_script(dedent(javascript))
except WebDriverException as wde:
if "document unloaded while waiting for result" in wde.msg:
# Wait a bit, and try again, when the browser has reloaded the page.
world.wait(1)
continue
else:
raise
return result
@world.absorb
def visit(url):
world.browser.visit(django_url(url))
wait_for_js_to_load()
@world.absorb
def url_equals(url):
return world.browser.url == django_url(url)
@world.absorb
def is_css_present(css_selector, wait_time=10):
return world.browser.is_element_present_by_css(css_selector, wait_time=wait_time)
@world.absorb
def is_css_not_present(css_selector, wait_time=5):
world.browser.driver.implicitly_wait(1)
try:
return world.browser.is_element_not_present_by_css(css_selector, wait_time=wait_time)
except:
raise
finally:
world.browser.driver.implicitly_wait(world.IMPLICIT_WAIT)
@world.absorb
def css_has_text(css_selector, text, index=0, strip=False):
"""
Return a boolean indicating whether the element with `css_selector`
has `text`.
If `strip` is True, strip whitespace at beginning/end of both
strings before comparing.
If there are multiple elements matching the css selector,
use `index` to indicate which one.
"""
# If we're expecting a non-empty string, give the page
# a chance to fill in text fields.
if text:
wait_for(lambda _: css_text(css_selector, index=index))
actual_text = css_text(css_selector, index=index)
if strip:
actual_text = actual_text.strip()
text = text.strip()
return actual_text == text
@world.absorb
def css_has_value(css_selector, value, index=0):
"""
Return a boolean indicating whether the element with
`css_selector` has the specified `value`.
If there are multiple elements matching the css selector,
use `index` to indicate which one.
"""
# If we're expecting a non-empty string, give the page
# a chance to fill in values
if value:
wait_for(lambda _: css_value(css_selector, index=index))
return css_value(css_selector, index=index) == value
@world.absorb
def wait_for(func, timeout=5, timeout_msg=None):
"""
Calls the method provided with the driver as an argument until the
return value is not False.
Throws an error if the WebDriverWait timeout clock expires.
Otherwise this method will return None.
"""
msg = timeout_msg or "Timed out after {} seconds.".format(timeout)
try:
WebDriverWait(
driver=world.browser.driver,
timeout=timeout,
ignored_exceptions=(StaleElementReferenceException)
).until(func)
except TimeoutException:
raise TimeoutException(msg)
@world.absorb
def wait_for_present(css_selector, timeout=30):
"""
Wait for the element to be present in the DOM.
"""
wait_for(
func=lambda _: EC.presence_of_element_located((By.CSS_SELECTOR, css_selector,)),
timeout=timeout,
timeout_msg="Timed out waiting for {} to be present.".format(css_selector)
)
@world.absorb
def wait_for_visible(css_selector, index=0, timeout=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):
"""
Wait for the element to be either invisible or not present on the DOM.
"""
wait_for(
func=lambda _: EC.invisibility_of_element_located((By.CSS_SELECTOR, css_selector,)),
timeout=timeout,
timeout_msg="Timed out waiting for {} to be invisible.".format(css_selector)
)
@world.absorb
def wait_for_clickable(css_selector, timeout=30):
"""
Wait for the element to be present and clickable.
"""
wait_for(
func=lambda _: EC.element_to_be_clickable((By.CSS_SELECTOR, css_selector,)),
timeout=timeout,
timeout_msg="Timed out waiting for {} to be clickable.".format(css_selector)
)
@world.absorb
def css_find(css, wait_time=30):
"""
Wait for the element(s) as defined by css locator
to be present.
This method will return a WebDriverElement.
"""
wait_for_present(css_selector=css, timeout=wait_time)
return world.browser.find_by_css(css)
@world.absorb
def css_click(css_selector, index=0, wait_time=30, dismiss_alert=False):
"""
Perform a click on a CSS selector, first waiting for the element
to be present and clickable.
This method will return True if the click worked.
If `dismiss_alert` is true, dismiss any alerts that appear.
"""
wait_for_clickable(css_selector, timeout=wait_time)
wait_for_visible(css_selector, index=index, timeout=wait_time)
assert_true(
css_visible(css_selector, index=index),
msg="Element {}[{}] is present but not visible".format(css_selector, index)
)
retry_on_exception(lambda: css_find(css_selector)[index].click())
# Dismiss any alerts that occur.
# We need to do this before calling `wait_for_js_to_load()`
# to avoid getting an unexpected alert exception
if dismiss_alert:
world.browser.get_alert().accept()
wait_for_js_to_load()
return True
@world.absorb
def css_check(css_selector, wait_time=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.
"""
css_click(css_selector=css_selector, wait_time=wait_time)
wait_for(lambda _: css_find(css_selector).selected)
return True
@world.absorb
def select_option(name, value, wait_time=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)
css_click(css_selector=css_selector, wait_time=wait_time)
wait_for(lambda _: css_has_value(select_css, value))
return True
@world.absorb
def id_click(elem_id):
"""
Perform a click on an element as specified by its id
"""
css_click('#{}'.format(elem_id))
@world.absorb
def css_fill(css_selector, text, index=0):
"""
Set the value of the element to the specified text.
Note that this will replace the current value completely.
Then for synchronization purposes, wait for the value on the page.
"""
wait_for_visible(css_selector, index=index)
retry_on_exception(lambda: css_find(css_selector)[index].fill(text))
wait_for(lambda _: css_has_value(css_selector, text, index=index))
return True
@world.absorb
def click_link(partial_text, index=0):
retry_on_exception(lambda: world.browser.find_link_by_partial_text(partial_text)[index].click())
wait_for_js_to_load()
@world.absorb
def click_link_by_text(text, index=0):
retry_on_exception(lambda: world.browser.find_link_by_text(text)[index].click())
@world.absorb
def css_text(css_selector, index=0, timeout=30):
# Wait for the css selector to appear
if is_css_present(css_selector):
return retry_on_exception(lambda: css_find(css_selector, wait_time=timeout)[index].text)
else:
return ""
@world.absorb
def css_value(css_selector, index=0):
# Wait for the css selector to appear
if is_css_present(css_selector):
return retry_on_exception(lambda: css_find(css_selector)[index].value)
else:
return ""
@world.absorb
def css_html(css_selector, index=0):
"""
Returns the HTML of a css_selector
"""
assert is_css_present(css_selector)
return retry_on_exception(lambda: css_find(css_selector)[index].html)
@world.absorb
def css_has_class(css_selector, class_name, index=0):
return retry_on_exception(lambda: css_find(css_selector)[index].has_class(class_name))
@world.absorb
def css_visible(css_selector, index=0):
assert is_css_present(css_selector)
return retry_on_exception(lambda: css_find(css_selector)[index].visible)
@world.absorb
def dialogs_closed():
def are_dialogs_closed(_driver):
'''
Return True when no modal dialogs are visible
'''
return not css_visible('.modal')
wait_for(are_dialogs_closed)
return not css_visible('.modal')
@world.absorb
def save_the_html(path='/tmp'):
url = world.browser.url
html = world.browser.html.encode('ascii', 'ignore')
filename = "{path}/{name}.html".format(path=path, name=quote_plus(url))
with open(filename, "w") as f:
f.write(html)
@world.absorb
def click_course_content():
course_content_css = 'li.nav-course-courseware'
css_click(course_content_css)
@world.absorb
def click_course_settings():
course_settings_css = 'li.nav-course-settings'
css_click(course_settings_css)
@world.absorb
def click_tools():
tools_css = 'li.nav-course-tools'
css_click(tools_css)
@world.absorb
def is_mac():
return platform.mac_ver()[0] is not ''
@world.absorb
def is_firefox():
return world.browser.driver_name is 'Firefox'
@world.absorb
def trigger_event(css_selector, event='change', index=0):
world.browser.execute_script("$('{}:eq({})').trigger('{}')".format(css_selector, index, event))
@world.absorb
def retry_on_exception(func, max_attempts=5, ignored_exceptions=StaleElementReferenceException):
"""
Retry the interaction, ignoring the passed exceptions.
By default ignore StaleElementReferenceException, which happens often in our application
when the DOM is being manipulated by client side JS.
Note that ignored_exceptions is passed directly to the except block, and as such can be
either a single exception or multiple exceptions as a parenthesized tuple.
"""
attempt = 0
while attempt < max_attempts:
try:
return func()
except ignored_exceptions:
world.wait(1)
attempt += 1
assert_true(attempt < max_attempts, 'Ran out of attempts to execute {}'.format(func))
@world.absorb
def disable_jquery_animations():
"""
Disable JQuery animations on the page. Any state changes
will occur immediately to the final state.
"""
# Ensure that jquery is loaded
world.wait_for_js_to_load()
# Disable jQuery animations
world.browser.execute_script("jQuery.fx.off = true;")