Added wait for JS on upload page Fixed an issue where css_click wasn't waiting for JS to load
612 lines
19 KiB
Python
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;")
|