Skip to content
This repository has been archived by the owner on Jul 13, 2024. It is now read-only.

Element query proposal #25

Closed
stevepryde opened this issue Sep 26, 2020 · 2 comments
Closed

Element query proposal #25

stevepryde opened this issue Sep 26, 2020 · 2 comments

Comments

@stevepryde
Copy link
Owner

When it comes to querying elements, the built-in WebDriver element selectors are good but often we need more advanced functionality, for example polling or filtering based on some predicate.

In my experience I have found it better to turn off implicit waits and implement polling separately outside of selenium. This provides far greater control over element queries, including powerful any/all filtering or polling multiple selectors in parallel and returning whichever element is found first (useful when CSS has changed between old/new versions of a website).

This also allows for different retry criteria. You might wish to retry a set number of times regardless of how long it takes, or you might only care about retrying within a set duration, or you might want some combination of the two (to avoid the scenario where a slow network means you never actually retried due to the first query taking too long).

One possibility is a functional approach, which should allow for a composable queries. Something like this (none of this has been tested):

fn poll_query<F>(timeout: Duration, interval: Duration, f: F) -> F
where 
    F: Fn() -> WebDriverResult<Vec<WebElement>>
{
    return || {
        let now = Instant::now();
        while (now.elapsed() < timeout) {
            match f() {
                Ok(x) => return Ok(x),
                Err(WebDriverError::NoSuchElement(_)) => break,
                Err(e) => return Err(e)
            }
            sleep(interval);
        }
        // Always do one last find.
        f()
    }
}

let found = poll_query(Duration::new(10, 0), Duration::new(0, 200), || e.find_elements(By::Tag("sometag")))?;

Another possibility is a builder approach, which would allow something like this:

let found = ElementQuery(driver).poll(Duration::new(10, 0), Duration::new(0, 200)).find(By::Tag("sometag"))?;

In both cases you could reduce the code required by having default timeout/interval that can be configured/overridden as needed.

So that would shorten to something like:
Functional version:

let found = poll_query(|| e.find_elements(By::Tag("sometag")))?;

Builder version:

let found = ElementQuery(driver).poll().find(By::Tag("sometag"))?;

Both would allow further expansion using functions such as:
find_any(Vec<By>) - return the first one that is non-empty - if nested below poll() this will execute both queries once per poll iteration.
with_retry(num_tries: u32) - retry the query a specified number of times if not found, rather than using a fixed timeout.
poll_missing() - poll until the query returns an empty vec.

Also since these return a Vec you can easily iterate and filter as needed as well. The filtering could even be applied at any stage of the pipeline (easier in the functional version).

Personally I prefer the functional version since it allows for easier composition and it's easier to add your own functions. There are some pros and cons to each pattern - and I'm not opposed to supporting both (the builder pattern would simply wrap the functional pattern).

All of this will go in a separate crate at least until it is deemed stable. That allows for more experimentation and rapid version bumps without affecting this crate, which by now should be looking at stabilization as much as possible.

@stevepryde
Copy link
Owner Author

This is turning out to be much more difficult than expected due to the existing design of thirtyfour :(

Closing this one and will follow up in thirtyfour_query if a solution presents itself.

@stevepryde
Copy link
Owner Author

Managed to get this working - see the thirtyfour_query crate!
https://crates.io/crates/thirtyfour_query

With the new query interface you can do things like:

let elem_text =
        driver.query(By::Css("thiswont.match")).or(By::Id("searchInput")).first().await?;

This will execute both queries once per poll iteration and return the first one that matches.
You can also filter on one or both match arms like this:

driver.query(By::Css("thiswont.match")).with_text("testing").or(By::Id("searchInput")).with_class("search").and_not_enabled().first().await?;

To fetch all matching elements instead of just the first one, simply change first() to all() and you'll get a Vec instead. This will never return an empty Vec. If either first() or all() don't match anything, you'll get WebDriverError::NoSuchElement instead. The error message will show the selectors used.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant