Python/Selenium: Waiting for combinations
Selenium has a feature where you can wait for a condition. Documentation for that is here. The expected conditions of an explicit wait offer a limited set to check. In some cases, it is beneficial to wait for a combination of conditions. For example, after submitting a form, you might get an error or you might get a success message. It is better to not have to wait for a timeout for one element if you instead receive the other. Here are a couple of expected condition classes that allow this.
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import StaleElementReferenceException
class wait_for_any(object):
def __init__(self, methods):
self.methods = methods
def __call__(self, driver):
try:
for method in self.methods:
if method(driver):
return True
return False
except StaleElementReferenceException:
return False
class wait_for_all(object):
def __init__(self, methods):
self.methods = methods
def __call__(self, driver):
try:
for method in self.methods:
if not method(driver):
return False
return True
except StaleElementReferenceException:
return False
Here is an example of waiting until the first of two different elements is visible. (Example lines split for clarity.)
methods = []
methods.append(EC.visibility_of_element_located(BY.ID, "idSuccess"))
methods.append(EC.visibility_of_element_located(BY.ID, "idError"))
method = wait_for_any(methods)
WebDriverWait(driver, 5).until(method)
Because wait_for_all
and wait_for_any
are themselves wait conditions, you can combine and nest them. You can have a condition combination like this: ((A and B and C) or (X and Y and Z))
The outer wait_for_any
condition checks two inner wait_for_all
conditions, each with their own combination of conditions.
Here is an example of using a combination to wait for an element to appear within a frame inside of the main document. (Example lines split for clarity.)
methods = []
methods.append(EC.frame_to_be_available_and_switch_to_it(None))
methods.append(EC.frame_to_be_available_and_switch_to_it(BY.ID, "newFrame"))
methods.append(EC.visibility_of_element_located(BY.ID, "childElement"))
method = wait_for_all(methods)
WebDriverWait(driver, 5).until(method)
In this example, the None
argument to the first wait switches to the defaultContent
frame. Once the newFrame
exists, switch to it. Once within the newFrame
, check the visibility of the child element. If the child element is not yet visible, start the next cycle by switching to the defaultContent
frame again.
Element Locator: I only used the By.ID
element locator as an example. Since each expected condition is created separately, each can use a locator value appropriate for that element. One can use By.ID
. Another may use By.XPATH
, By.NAME
, By.CLASS_NAME
, By.TAG_NAME
, By.CSS_SELECTOR
, etc.
Frame Switch Caveat: A TimeoutException
may occur after any of the condition checks. If this happens, you do not know which frame is current. Thus, if that happens, it would be safest to reset the frame before continuing.
Element Caveat: The combined conditions can reference many different target elements. Because of this, it is not safe to rely on the element returned from WebDriverWait
. You may not know which target element it points to.