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?

undetected Selenium
  • 183,867
  • 41
  • 278
  • 352
  • 1
    *to implement WebDriverWait for two expected_conditions*... Which expected conditions? – Andersson Nov 25 '18 at 13:21
  • @Andersson good question...(in my answer I assumed `presence_of_element_located` and `element_to_be_clickable`). – Moshe Slavin Nov 25 '18 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... – Moshe Slavin Nov 25 '18 at 14:05
  • @Andersson Thanks for looking. I have updated the question for brevity. Let me know if you need more details. – undetected Selenium Nov 26 '18 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. – Todor Minakov Nov 28 '18 at 09:54
  • @Todor There is no significant difference in the question with respect to the first version of this question. The requirement is clear _Method Chaining_ of _WebDriverWaits_ through _lambda_ expressions. The changes in the verbatim is to reduce the grey area and aiming for canonical answers. – undetected Selenium Nov 28 '18 at 10:12

3 Answers3

4

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()
ewwink
  • 18,382
  • 2
  • 44
  • 54
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']]"))
Todor Minakov
  • 19,097
  • 3
  • 55
  • 60
  • 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? – undetected Selenium Nov 28 '18 at 09:42
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.

Moshe Slavin
  • 5,127
  • 5
  • 23
  • 38