Search code examples
pythonseleniumexpected-condition

Python Selenium: How to apply "And" logical operator in expected condition


Basically, what I want to do is search https://www.ssrn.com/index.cfm/en/ for a name and then click on some partially matching name. For example, I want to search "John Doe". SSRN will return a list of papers that contain John Doe in their author list. Below is my code to try and do that.

name = "John Doe"
try:
    element = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.PARTIAL_LINK_TEXT, name.split(' ', 1)[0])) and
        EC.presence_of_element_located((By.PARTIAL_LINK_TEXT, name.split(' ', 1)[1]))
    )
    # I want the element to be clicked only if both the first name and last name appear.
    element.click()
except:
    print("No result")

It works if John Doe is a full match, i.e. the author list looks like (John Doe, x, y, z). It works if John Doe is a partial match and there are no other partial matches, i.e. the author list looks like (John M. Doe, x, y, z). However, it breaks if there are multiple partial matches. For example, if the list looks like (Jane Doe, John Doe, y, z). Then, my code will select Jane Doe. I think I want something similar to Java's EC.and(), but I'm not sure how to implement it or if there's a better way to do this. Thank you!


Solution

  • The snippet

    EC.presence_of_element_located((By.PARTIAL_LINK_TEXT, name.split(' ', 1)[0])) and
    EC.presence_of_element_located((By.PARTIAL_LINK_TEXT, name.split(' ', 1)[1]))
    

    just evaluates to

    EC.presence_of_element_located((By.PARTIAL_LINK_TEXT, name.split(' ', 1)[1]))
    

    So it always only checks that condition and that condition only, i.e it always only tries to find Doe, completely ignoring John. Which is why you find Jane Doe since it appears before.

    This is not how you check for multiple conditions, you need to pass a function-like object to .until, that can check for multiple conditions and return a truthy/falsy value.

    For your specific needs that function could look like-

    def presence_of_element_with_both(driver):
        name = "John Doe"
        first = EC.presence_of_element_located((By.PARTIAL_LINK_TEXT, name.split(' ', 1)[0]))(driver)
        second = EC.presence_of_element_located((By.PARTIAL_LINK_TEXT, name.split(' ', 1)[1]))(driver)
        if first == second:
            # Both elements exist and they are the same
            return first
        else:
            return False    
    

    This will try finding an element with partial link text "John" and then it will try finding an element with partial link text "Doe" - if both of the elements are found and if both point to the same element - you're gold.

    You can use it in your until like so-

    WebDriverWait(driver, 10).until(presence_of_element_with_both)
    

    You might, however, find it convenient to generalize this-

    def presence_of_element_with_all(locators):
        def inner(driver):
            # Get all the elements that match each given locator
            results = [EC.presence_of_element_located(locator)(driver) for locator in locators]
            # Check if all the elements are the same
            # If they are, all the conditions have been met
            # If they are not, all the conditions did not meet
            return results[0] if len(set(results)) == 1 else False
        return inner
    

    This will find the singular element that satisfies all locators given.

    You can use this like so-

    first_name, last_name = "John Doe".split(' ')
    WebDriverWait(driver, 10).until(presence_of_element_with_all([(By.PARTIAL_LINK_TEXT, first_name), (By.PARTIAL_LINK_TEXT, last_name)]))
    

    Do note that, I'm using the closure pattern to do this. Internally, selenium uses a class with an __init__ and __call__ function to do the same - this is called a function like object and you can use this too if you want-

    class presence_of_element_with_all:
        def __init__(self, locators):
            self.locators = locators
        def __call__(self, driver):
            results = [EC.presence_of_element_located(locator)(driver) for locator in self.locators]
            # Check if all the elements are the same
            # If they are, all the conditions have been met
            # If they are not, all the conditions did not meet
            return results[0] if len(set(results)) == 1 else False
    

    You'd use this the exact same way as the closure pattern.