stevepryde/thirtyfour

Convenience method for performing actions in a new tab

bcpeinhardt opened this issue ยท 7 comments

Most of the time when I perform an operation in a new tab, I simply need to open a new tab, do some stuff, then close it and return to the original. A convenience method or trait on WebDriver to do this would go a long way to decluttering some of my code. I quickly wrote up the simple function version of what I'm talking about.

use std::future::Future;
use thirtyfour::prelude::*;
use tokio;

pub async fn in_new_tab<F, Fut, T>(driver: &WebDriver, f: F) -> WebDriverResult<T>
where
    F: FnOnce() -> Fut,
    Fut: Future<Output = WebDriverResult<T>>,
{
    // Opening Tab ----------------------------------------
    let handle = driver.current_window_handle().await?;
    driver
        .execute_script(r#"window.open("about:blank", target="_blank");"#)
        .await?;
    let handles = driver.window_handles().await?;
    driver.switch_to().window(&handles[1]).await?;
    // ----------------------------------------------------

    let result = f().await;

    // Closing Tab --------------------------------------------
    driver.execute_script(r#"window.close();"#).await?;
    driver.switch_to().window(&handle).await?;
    // --------------------------------------------------------

    result
}

#[tokio::main]
async fn main() -> WebDriverResult<()> {
    let caps = DesiredCapabilities::firefox();
    let driver = WebDriver::new(&format!("http://localhost:{}/wd/hub", "4444"), &caps).await?;

    driver.get("https://google.com").await?;

    in_new_tab(&driver, || {
        async {
            driver.get("https://wikipedia.com").await?;
            Ok(())
        }
    }).await?;

    driver.get("https://youtube.com").await?;
    driver.quit().await?;

    Ok(())
}

I started to write an async_trait to implement on WebDriver but I ran into issues with the closure not being Send. What do you think about something similar to this becoming part of the WebDriverCommands trait? So the we could have something like

driver.in_new_tab(|| { async { Ok(()) } }).await?;

or for the nightly folks with async closures

driver.in_new_tab(async || { Ok(()) }).await?;

I agree that this would be a pretty handy thing to have. I think the issues you mentioned could be similar the issues I had getting the ElementQuery and ElementWaiter closures to work, so maybe there could be a nice solution.

@bcpeinhardt Thanks to your code above I was able to implement this in WebDriverCommands.

The trick to getting the closure working seemed to be requiring a Send trait bound on all of the generic params like this:

async fn in_new_tab<F, Fut, T>(&self, f: F) -> WebDriverResult<T>
    where
        F: FnOnce() -> Fut + Send,
        Fut: Future<Output = WebDriverResult<T>> + Send,
        T: Send,

See the in_new_tab branch. I was able to do the following in a quick test (not included in the branch):

let t = driver.in_new_tab(|| async {
    driver.get("https://www.google.com").await?;
    sleep(Duration::from_secs(5)).await;
    driver.title().await
}).await?;
println!("Window title: {}", t);

Of course you can return Ok(()) from the async block as well, but this was to demonstrate passing a value back out.
The sleep was just so I could confirm that it works correctly.

Let me know if this works for you, and if so I can merge it in ๐Ÿ™‚

TOML

[package]
name = "testing_in_new_tab_web_command"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
thirtyfour = { git = "https://github.com/stevepryde/thirtyfour.git", branch = "in_new_tab"}
tokio = "1.12.0"

Code

use thirtyfour::prelude::*;
use tokio;

#[tokio::main]
async fn main() -> WebDriverResult<()> {
    let caps = DesiredCapabilities::firefox();
    let driver = WebDriver::new("http://localhost:4444/wd/hub", &caps).await?;

    let stars_count = driver
        .in_new_tab(|| async {
            driver
                .get("https://github.com/stevepryde/thirtyfour")
                .await?;
            let stars = driver
                .query(By::XPath("//a[@class=\"social-count js-social-count\"]"))
                .first()
                .await?
                .text()
                .await?;
            Ok(stars)
        })
        .await?;
    println!(
        "Thirtyfour has {} stars on github, a number which will likely grow.",
        stars_count
    );

    driver.quit().await?;

    Ok(())
}

Works like a charm!!!! This is great, thank you!!!!!

Awesome. I have some more changes I need to make to it, to make it more robust. For example currently it assumes there are no tabs already open, and that handles[1] is the new tab. Also I'd like to add a doctest for it.

I think it probably also needs a warning that this will temporarily switch the context to another tab so any selenium operations executed during this time will point at the new tab and not the existing one (in case someone tries using threads or join! etc).

Yup! Out of curiosity, what is the use case for accessing the same &WebDriver in multiple threads? In a sense, the WebDriver commands "mutate" the browser when they click a link or navigate to a new url etc, even though they have an &self in their function signatures. Combining that with the conversation we had before, where you mentioned that Selenium calls from the same driver are returned FIFO rather than asynchronously, I'm not sure what the use case for accessing the same &WebDriver across threads is.
It would be interesting if there were a way to specify setup up and tear down as blocking operations for entry and exit from a thread. In this case one would want Wake -> Switch to named handle (can drivers store named handles?) -> Make Progress -> Switch to previous handle -> Yield. I suspect a setup like this could be achieved with some funky nested threading.

There isn't really a good use case for accessing the same WebDriver in multiple threads. Potentially if you were just reading elements in parallel from a static page that might be ok, but I'd consider that an edge case. So probably this doesn't need a warning for that.

Added in v0.27.2 ๐Ÿ™‚