Commit c3a957b4 by Jay Zoldak

Improve retry logic for Stale Element Exceptions

Better exception retry logic

Change methods to lambdas
parent d3dc9d27
...@@ -67,7 +67,7 @@ def get_index(name): ...@@ -67,7 +67,7 @@ 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 all_pages[i].html == '\n {name}\n'.format(name=name): if world.retry_on_exception(lambda: all_pages[i].html) == '\n {name}\n'.format(name=name):
return i return i
return None return None
......
...@@ -6,6 +6,7 @@ import time ...@@ -6,6 +6,7 @@ import time
import platform import platform
from urllib import quote_plus from urllib import quote_plus
from selenium.common.exceptions import WebDriverException, TimeoutException from selenium.common.exceptions import WebDriverException, TimeoutException
from selenium.common.exceptions import StaleElementReferenceException
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
...@@ -45,18 +46,25 @@ def css_has_text(css_selector, text, index=0): ...@@ -45,18 +46,25 @@ def css_has_text(css_selector, text, index=0):
@world.absorb @world.absorb
def wait_for(func, timeout=5): def wait_for(func, timeout=5):
WebDriverWait(world.browser.driver, timeout).until(func) WebDriverWait(
driver=world.browser.driver,
timeout=timeout,
ignored_exceptions=(StaleElementReferenceException)
).until(func)
def wait_for_present(css_selector, timeout=30): def wait_for_present(css_selector, timeout=30):
""" """
Waiting for the element to be present in the DOM. Waiting for the element to be present in the DOM.
Throws an error if the wait_for time expires. Throws an error if the WebDriverWait timeout clock expires.
Otherwise this method will return None Otherwise this method will return None
""" """
try: try:
WebDriverWait(driver=world.browser.driver, WebDriverWait(
timeout=60).until(EC.presence_of_element_located((By.CSS_SELECTOR, css_selector,))) driver=world.browser.driver,
timeout=timeout,
ignored_exceptions=(StaleElementReferenceException)
).until(EC.presence_of_element_located((By.CSS_SELECTOR, css_selector,)))
except TimeoutException: except TimeoutException:
raise TimeoutException("Timed out waiting for {} to be present.".format(css_selector)) raise TimeoutException("Timed out waiting for {} to be present.".format(css_selector))
...@@ -65,12 +73,15 @@ def wait_for_present(css_selector, timeout=30): ...@@ -65,12 +73,15 @@ def wait_for_present(css_selector, timeout=30):
def wait_for_visible(css_selector, timeout=30): def wait_for_visible(css_selector, timeout=30):
""" """
Waiting for the element to be visible in the DOM. Waiting for the element to be visible in the DOM.
Throws an error if the wait_for time expires. Throws an error if the WebDriverWait timeout clock expires.
Otherwise this method will return None Otherwise this method will return None
""" """
try: try:
WebDriverWait(driver=world.browser.driver, WebDriverWait(
timeout=timeout).until(EC.visibility_of_element_located((By.CSS_SELECTOR, css_selector,))) driver=world.browser.driver,
timeout=timeout,
ignored_exceptions=(StaleElementReferenceException)
).until(EC.visibility_of_element_located((By.CSS_SELECTOR, css_selector,)))
except TimeoutException: except TimeoutException:
raise TimeoutException("Timed out waiting for {} to be visible.".format(css_selector)) raise TimeoutException("Timed out waiting for {} to be visible.".format(css_selector))
...@@ -79,12 +90,15 @@ def wait_for_visible(css_selector, timeout=30): ...@@ -79,12 +90,15 @@ def wait_for_visible(css_selector, timeout=30):
def wait_for_invisible(css_selector, timeout=30): def wait_for_invisible(css_selector, timeout=30):
""" """
Waiting for the element to be either invisible or not present on the DOM. Waiting for the element to be either invisible or not present on the DOM.
Throws an error if the wait_for time expires. Throws an error if the WebDriverWait timeout clock expires.
Otherwise this method will return None Otherwise this method will return None
""" """
try: try:
WebDriverWait(driver=world.browser.driver, WebDriverWait(
timeout=timeout).until(EC.invisibility_of_element_located((By.CSS_SELECTOR, css_selector,))) driver=world.browser.driver,
timeout=timeout,
ignored_exceptions=(StaleElementReferenceException)
).until(EC.invisibility_of_element_located((By.CSS_SELECTOR, css_selector,)))
except TimeoutException: except TimeoutException:
raise TimeoutException("Timed out waiting for {} to be invisible.".format(css_selector)) raise TimeoutException("Timed out waiting for {} to be invisible.".format(css_selector))
...@@ -93,14 +107,17 @@ def wait_for_invisible(css_selector, timeout=30): ...@@ -93,14 +107,17 @@ def wait_for_invisible(css_selector, timeout=30):
def wait_for_clickable(css_selector, timeout=30): def wait_for_clickable(css_selector, timeout=30):
""" """
Waiting for the element to be present and clickable. Waiting for the element to be present and clickable.
Throws an error if the wait_for time expires. Throws an error if the WebDriverWait timeout clock expires.
Otherwise this method will return None. Otherwise this method will return None.
""" """
# Sometimes the element is clickable then gets obscured. # Sometimes the element is clickable then gets obscured.
# In this case, pause so that it is not reported clickable too early # In this case, pause so that it is not reported clickable too early
try: try:
WebDriverWait(world.browser.driver, WebDriverWait(
timeout=timeout).until(EC.element_to_be_clickable((By.CSS_SELECTOR, css_selector,))) driver=world.browser.driver,
timeout=timeout,
ignored_exceptions=(StaleElementReferenceException)
).until(EC.element_to_be_clickable((By.CSS_SELECTOR, css_selector,)))
except TimeoutException: except TimeoutException:
raise TimeoutException("Timed out waiting for {} to be clickable.".format(css_selector)) raise TimeoutException("Timed out waiting for {} to be clickable.".format(css_selector))
...@@ -133,7 +150,7 @@ def css_click(css_selector, index=0, wait_time=30): ...@@ -133,7 +150,7 @@ def css_click(css_selector, index=0, wait_time=30):
# another element might be on top of it. In this case, try # another element might be on top of it. In this case, try
# clicking in the upper left corner. # clicking in the upper left corner.
try: try:
return world.css_find(css_selector)[index].click() return retry_on_exception(lambda: world.css_find(css_selector)[index].click())
except WebDriverException: except WebDriverException:
return css_click_at(css_selector, index=index) return css_click_at(css_selector, index=index)
...@@ -177,19 +194,19 @@ def id_click(elem_id): ...@@ -177,19 +194,19 @@ def id_click(elem_id):
@world.absorb @world.absorb
def css_fill(css_selector, text, index=0): def css_fill(css_selector, text, index=0):
css_find(css_selector)[index].fill(text) retry_on_exception(lambda: css_find(css_selector)[index].fill(text))
@world.absorb @world.absorb
def click_link(partial_text, index=0): def click_link(partial_text, index=0):
world.browser.find_link_by_partial_text(partial_text)[index].click() retry_on_exception(lambda: world.browser.find_link_by_partial_text(partial_text)[index].click())
@world.absorb @world.absorb
def css_text(css_selector, index=0, timeout=30): def css_text(css_selector, index=0, timeout=30):
# Wait for the css selector to appear # Wait for the css selector to appear
if is_css_present(css_selector): if is_css_present(css_selector):
return css_find(css_selector, wait_time=timeout)[index].text return retry_on_exception(lambda: css_find(css_selector, wait_time=timeout)[index].text)
else: else:
return "" return ""
...@@ -198,7 +215,7 @@ def css_text(css_selector, index=0, timeout=30): ...@@ -198,7 +215,7 @@ def css_text(css_selector, index=0, timeout=30):
def css_value(css_selector, index=0): def css_value(css_selector, index=0):
# Wait for the css selector to appear # Wait for the css selector to appear
if is_css_present(css_selector): if is_css_present(css_selector):
return css_find(css_selector)[index].value return retry_on_exception(lambda: css_find(css_selector)[index].value)
else: else:
return "" return ""
...@@ -209,18 +226,18 @@ def css_html(css_selector, index=0): ...@@ -209,18 +226,18 @@ def css_html(css_selector, index=0):
Returns the HTML of a css_selector Returns the HTML of a css_selector
""" """
assert is_css_present(css_selector) assert is_css_present(css_selector)
return css_find(css_selector)[index].html return retry_on_exception(lambda: css_find(css_selector)[index].html)
@world.absorb @world.absorb
def css_has_class(css_selector, class_name, index=0): def css_has_class(css_selector, class_name, index=0):
return css_find(css_selector)[index].has_class(class_name) return retry_on_exception(lambda: css_find(css_selector)[index].has_class(class_name))
@world.absorb @world.absorb
def css_visible(css_selector, index=0): def css_visible(css_selector, index=0):
assert is_css_present(css_selector) assert is_css_present(css_selector)
return css_find(css_selector)[index].visible return retry_on_exception(lambda: css_find(css_selector)[index].visible)
@world.absorb @world.absorb
...@@ -277,15 +294,21 @@ def trigger_event(css_selector, event='change', index=0): ...@@ -277,15 +294,21 @@ def trigger_event(css_selector, event='change', index=0):
@world.absorb @world.absorb
def retry_on_exception(func, max_attempts=5): 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 attempt = 0
while attempt < max_attempts: while attempt < max_attempts:
try: try:
return func() return func()
break break
except WebDriverException: except ignored_exceptions:
world.wait(1) world.wait(1)
attempt += 1 attempt += 1
except:
attempt += 1
assert_true(attempt < max_attempts, 'Ran out of attempts to execute {}'.format(func)) assert_true(attempt < max_attempts, 'Ran out of attempts to execute {}'.format(func))
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