1

Building off of the answer to How to wait until the page is loaded with Selenium for Python? I am attempting to create a method that allows multiple elements to be polled for presence using Expected Conditions.

I receive an error 'bool' object is not callable on the line containing: wait.until(any(expectations)).

The thought process was to allow an number of Xpaths to be passed as expected conditions, then using any(), in a similar manner to this java based answer, Trying to wait for one of two elements in the page using selenium xpath, check if any of the conditions are present.

What would be the proper way use any() in this case? Or better yet, what needs to be done for this method to work?

Assume that the Selenium .get('url') has already been executed immediately prior to calling this method.

def wait_with_xpath_expectation(self, search_elements, timeout=6, max_attempts=3):
    """
    Selenium wait for an element(s) designated by Xpath to become available in the DOM. Useful for javascript AJAXy
    loading pages where content may be be rendered dynamically on the client side after page load appears complete.
    search_elements may be one Xpath as a string or many as a list. This allows for any of multiple elements on a
    page or pages to be determined to have loaded based on expectations.
    :param search_elements: string or list (strings converted to lists), Xpath(s)
    :param timeout: int, seconds
    :param max_attempts: int, time to wait before giving up on page loading
    :return: Boolean, True if page has loaded, False if all attempts have failed
    """

    # Page is not loaded yet
    loaded = False

    # Initialize attempt count
    attempt = 0

    # If only one element has been passed, ensure it is encapsulated by a list
    if type(search_elements) is str:
        search_elements = [search_elements]

    # Begin the polling loop
    while attempt < max_attempts:

        try:

            while loaded is False:
                # Create a list of expected elements using list comprehension
                expectations = [EC.presence_of_element_located((By.XPATH, element)) for element in search_elements]

                # Define wait
                wait = WebDriverWait(self.browser, timeout)

                # Execute
                wait.until(any(expectations))

                # Acknowledge load
                loaded = True

                print("Success: Page loaded based on expectations")

                # Exit returning True for load
                return loaded

        except TimeoutException as e:

            # Increment attempts
            attempt += 1

            # Check again if max attempts has not been exceeded
            while attempt < max_attempts:

                # Increase timeout by 20%
                timeout *= .2

                # Try again 
                continue

            # Print an error if max attempts is exceeded
            print("Error: Max load with expectations attempts exceeded,", e)

            # Exit returning False for load
            return loaded
Liquidgenius
  • 520
  • 4
  • 13
  • 27
  • Additional research turned up the fact that I could perform a union of nodes and combine the Xpath addresses with | into one Xpath. https://stackoverflow.com/questions/5350666/xpath-or-operator-for-different-nodes. Still working on a solution. – Liquidgenius Aug 01 '18 at 20:46

2 Answers2

4

You can have an expected condition class to wait for a combination of expected conditions. Here is an example of one.

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

This would then be used by building an array of the expected conditions and checking for all of them in the same wait. (Example lines split for clarity.)

methods = []
methods.append(EC.visibility_of_element_located(BY.ID, "idElem1"))
methods.append(EC.visibility_of_element_located(BY.ID, "idElem2"))
method = wait_for_all(methods)
WebDriverWait(driver, 5).until(method)

This will perform one five second wait while checking for visibility of two different elements.

I have documented this further in a blog post here.

Tim Butterfield
  • 236
  • 1
  • 3
  • Hi Tim. I like this implementation much better than mine, thank you! Multiple DOM elements can be waited for explicitly instead of a union. On top of that it looks very easy to use. The one item I'd change is BY.ID to BY.XPATH as I've found that some sites do not properly use class and id tags, but XPATH is always present due to the DOM. Accepting answer, thank you. – Liquidgenius Aug 15 '18 at 16:39
  • Thanks. I just used By.ID as example element locator type. Since each expected condition is created separately, you can combine different locator types as appropriate for that element. One element may be located By.ID, another could use By.XPATH, By.NAME, By.CLASS_NAME, By.TAG_NAME, By.CSS_SELECTOR, etc. – Tim Butterfield Aug 16 '18 at 18:24
  • Accepting answer – Liquidgenius Nov 01 '18 at 20:19
  • This is perfect answer but now selenium expected condition function visibility_of_element_located takes argument as 1 tuple. – Selim Mıdıkoğlu Aug 15 '20 at 06:47
0

Once I realized Xpath has the ability for unions, the following method works. I will leave this answer open in case anyone else has a better one.

def wait_with_xpath_expectation(self, search_elements, timeout=6, max_attempts=3):
    """
    Selenium wait for an element designated by Xpath to become available in the DOM. Useful for javascript AJAXy
    loading pages where content may be be rendered dynamically on the client side after page load appears complete.
    search_elements may be one Xpath as a string or many as a list. This allows for any of multiple elements on a
    page or pages to be determined to have loaded based on expectations.
    :param search_elements: string or list (strings converted to lists), Xpath(s)
    :param timeout: int, seconds
    :param max_attempts: int, time to wait before giving up on page loading
    :return: Boolean, True if page has loaded, False if all attempts have failed
    """

    # Page is not loaded yet
    loaded = False

    # Initialize attempt count
    attempt = 0

    # If only one element has been passed, ensure it is encapsulated by a list
    if type(search_elements) is str:
        search_elements = [search_elements]

    # Begin the polling loop
    while attempt < max_attempts:

        try:

            while loaded is False:
                # Decompose the list of Xpaths to a union of nodes
                node_union = " | ".join(search_elements)

                expectation = EC.presence_of_element_located((By.XPATH, node_union))

                # Define wait
                wait = WebDriverWait(self.browser, timeout)

                # Execute
                wait.until(expectation)

                # Acknowledge load
                loaded = True

                print("Success: Page loaded based on expectations")

                # Exit returning True for load
                return loaded

        except TimeoutException as e:

            # Increment attempts
            attempt += 1

            # Check again if max attempts has not been exceeded
            while attempt < max_attempts:

                # Increase timeout by 20%
                timeout *= .2

                # Try again
                continue

            # Print an error if max attempts is exceeded
            print("Error: Max load with expectations attempts exceeded,", e)

            # Exit returning False for load
            return loaded
Liquidgenius
  • 520
  • 4
  • 13
  • 27