2

I am looking to implement method chaining of Selenium WebDriverWaits.

To start with, this block of code implementing a single WebDriverWait works perfect:

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait

options = webdriver.ChromeOptions() 
options.add_argument("start-maximized")
options.add_argument('disable-infobars')
driver = webdriver.Chrome(chrome_options=options, executable_path=r'C:\Utility\BrowserDrivers\chromedriver.exe')
driver.get('https://www.facebook.com')
element = WebDriverWait(driver, 5).until(lambda x: x.find_element_by_xpath("//input[@id='email']"))
element.send_keys("method_chaining")

As per as my current requirement I have to implement chaining of two WebDriverWait instances as the idea is to fetch the element returned from the first WebDriverWait as an input to the (chained) second WebDriverWait.

To achieve this, I followed the discussion method chaining in python tried to use the use Python's lambda function through method chaining using Pipe - Python library to use infix notation in Python.

Here is my code trial:

from pipe import *
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC

options = webdriver.ChromeOptions() 
options.add_argument("start-maximized")
options.add_argument('disable-infobars')
driver = webdriver.Chrome(chrome_options=options, executable_path=r'C:\WebDrivers\chromedriver.exe')
driver.get('https://www.facebook.com')
element = WebDriverWait(driver,15).until((lambda driver: driver.find_element_by_xpath("//input[@id='email']"))
        | where(lambda driver: driver.find_element_by_css_selector("table[role='presentation']")))  
element.send_keys("method_chaining")

I am seeing an error as:

DevTools listening on ws://127.0.0.1:52456/devtools/browser/e09c1d5e-35e3-4c00-80eb-cb642fa273ad
Traceback (most recent call last):
  File "C:\Users\Soma Bhattacharjee\Desktop\Debanjan\PyPrograms\python_pipe_example.py", line 24, in <module>
    | where(lambda driver: driver.find_elements(By.CSS_SELECTOR,"table[role='presentation']")))
  File "C:\Python\lib\site-packages\pipe.py", line 58, in __ror__
    return self.function(other)
  File "C:\Python\lib\site-packages\pipe.py", line 61, in <lambda>
    return Pipe(lambda x: self.function(x, *args, **kwargs))
  File "C:\Python\lib\site-packages\pipe.py", line 271, in where
    return (x for x in iterable if (predicate(x)))
TypeError: 'function' object is not iterable

Followed the following discussions:

Still no clue what I am missing.

Can someone guide me where I am going wrong?

6
  • 1
    to implement WebDriverWait for two expected_conditions... Which expected conditions? Commented Nov 25, 2018 at 13:21
  • @Andersson good question...(in my answer I assumed presence_of_element_located and element_to_be_clickable). Commented Nov 25, 2018 at 14:05
  • @DebanjanB In my answer, you can see some of your issues, I am sure you will have a more elegant way to implement it... Commented Nov 25, 2018 at 14:05
  • @Andersson Thanks for looking. I have updated the question for brevity. Let me know if you need more details. Commented Nov 26, 2018 at 14:20
  • 2
    The question keeps changing, quite a moving target :). The latest edit is "as the idea is to fetch the element returned from the first WebDriverWait as an input to the (chained) second WebDriverWait." - but in your attempt sample you're not using the first element, the wait is based off driver - so it's from the top of the DOM. Commented Nov 28, 2018 at 9:54

3 Answers 3

4
+100

Edit:

I don't know if it meet your requirement, but we need to create custom construct.

@Pipe
def where(i, p):
    if isinstance(i, list):
        return [x for x in i if p(x)]
    else:
        return i if p(i.find_element_by_xpath('//ancestor::*')) else None

element = WebDriverWait(driver, 5).until(lambda x: x.find_element_by_xpath("//input[@id='email']") \
        | where(lambda y: y.find_element_by_css_selector("table[role='presentation']")))  

element.send_keys("method_chaining")

old:


I'm not an expert but I think maybe you misunderstand with how pipe work, by default it work to process previous value, for example

original | filter = result
[a,b,c] | select(b) = b

what you want maybe is and operator

WebDriverWait(driver,15).until(
    lambda driver: driver.find_element(By.CSS_SELECTOR,"table[role='presentation']")
    and driver.find_element(By.XPATH,"//input[@id='email']"))

or selenium ActionChains but no wait method, we need to extend it

from selenium.webdriver import ActionChains

class Actions(ActionChains):
    def wait(self, second, condition):
        element = WebDriverWait(self._driver, second).until(condition)
        self.move_to_element(element)
        return self

Actions(driver) \
    .wait(15, EC.presence_of_element_located((By.CSS_SELECTOR, "table[role='presentation']"))) \
    .wait(15, EC.element_to_be_clickable((By.XPATH, "//input[@id='email']"))) \
    .click() \
    .send_keys('method_chaining') \
    .perform()
Sign up to request clarification or add additional context in comments.

Comments

4

As I gather your requirement is actually to have a WebDriverWait with two expected conditions, not precisely to use method chaining and the pipe library at all cost.

You can do that, by pushing the lambda expression syntax a bit. The cleanest solution is to make it an enumerable over function calls, and get the returned value of the one you need:

element = WebDriverWait(driver, 5).until(lambda x: (x.find_element_by_xpath("//input[@id='email']"), x.find_element_by_css_selector("table[role='presentation']"))[1])

With the index you'll receive the second tuple member, e.g. the table element.

The expression can be rewritten to a boolean:

element = WebDriverWait(driver, 5).until(lambda x: x.find_element_by_xpath("//input[@id='email']") and x.find_element_by_css_selector("table[role='presentation']"))

And you'll once again get the second element, the last part of the boolean. This implementation has some pitfalls though, when (ab)used with too complex boolean, so YMMV; I'd stick with the 1st one, the enumerable.

With this approach you get the benefits of WebDriverWait - if any of the calls in the tuple raises one of the handled exceptions there will be a rertry, etc.
There is one performance negative that comes out of this approach though - the call to the first method will be executed on every cycle, even if it has already passed successful and now the second condition is waited for.


And, here's a totally different alternative, the most clean solution - I see you're not shy to use xpath, so a pure xpath one.
As your aim is to get the table element, if and only if the input is present, this will do just that:

//table[@role='presentation' and ancestor::*//input[@id='email']]

This will select the table with the role. The other condition for it is to go up its ancestors - and the ancestor axis will go up to the top node in the DOM - and to find down from there an input element with that id attribute.

Thus if the input is still not present, the xpath will not match anything. The moment it's available, and there's also a table with that role value - it'll be returned.
Naturally, this selector can be used directly in the WebDriverWait with a single condition:

element = WebDriverWait(driver, 5).until(lambda x: x.find_element_by_xpath("//table[@role='presentation' and ancestor::*//input[@id='email']]"))

1 Comment

The first part of the answer was pretty much helpful. I have updated the verbatim of the requirement for more brevity. Can you have a re-look please?
1

As your Error shows:

TypeError: 'function' object is not iterable

This is because:

where() method Only yields the matching items of the given iterable for example [1, 2, 3] | where(lambda x: x % 2 == 0) | concat => '2'

Hope this answers Can someone guide me where I am going wrong?

EDIT:

If you want to use pipe.where: so you can do that with the same code you have just with some changes: Your element var should be something like this:

element = WebDriverWait(driver,15).until((lambda driver: driver.find_elements(By.XPATH,"//input[@id='email']"))) \
       | where(WebDriverWait(driver,15).until(lambda driver: driver.find_elements(By.CSS_SELECTOR,"table[role='presentation']")))

But you can't do anything with the element because it is a generator object.

The best thing to do for "to implement WebDriverWait for two expected conditions" is just to have two line's of WebDriverWait!

Like this:

presentation_element = WebDriverWait(driver, 20).until(EC.presence_of_element_located((By.CSS_SELECTOR,"table[role='presentation']")))
email_element = WebDriverWait(driver, 20).until(EC.element_to_be_clickable((By.XPATH, "//input[@id='email']")))
email_element.send_keys("method_chaining")

Hope this helps you...

Edit2:

I think what you are looking for is to construct your one Pipe as shown here.

2 Comments

Thanks @MosheSlavin I have updated the question for brevity. Can you please have a relook?
@DebanjanB I don't know if it's possible to do but you can try constructing your one Pipe...

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.