Introduction

Welcome to The Book for thirtyfour.

thirtyfour is a crate for automating Web Browsers in Rust using the WebDriver / Selenium ecosystem. It also provides some support for the Chrome DevTools Protocol, which is used by popular frameworks such as Cypress and Playwright.

Why is it called "thirtyfour" ?

Thirty-four (34) is the atomic number for the Selenium chemical element (Se) ⚛️.

Features

  • All W3C WebDriver V1 and WebElement methods are supported
  • Create new browser session directly via WebDriver (e.g. chromedriver)
  • Create new browser session via Selenium Standalone or Grid
  • Find elements (via all common selectors e.g. Id, Class, CSS, Tag, XPath)
  • Send keys to elements, including key-combinations
  • Execute Javascript
  • Action Chains
  • Get and set cookies
  • Switch to frame/window/element/alert
  • Shadow DOM support
  • Alert support
  • Capture / Save screenshot of browser or individual element as PNG
  • Chrome DevTools Protocol (CDP) support (limited)
  • Advanced query interface including explicit waits and various predicates
  • Component Wrappers (similar to Page Object Model)

Installation

To use the thirtyfour crate in your Rust project, you need to add it as a dependency in your Cargo.toml file:

[dependencies]
thirtyfour = "0.33.0-alpha.2"

To automate a web browser, thirtyfour needs to communicate with a webdriver server. You will need to download the appropriate webdriver server for your browser.

The webdriver may be zipped. Unzip it and place the webdriver binary somewhere in your PATH. Make sure it is executable and that you have permissions to run it.

You will also need the corresponding web browser to be installed in your Operating System. Make sure the webdriver version you download corresponds to the version of the browser you have installed.

If the webdriver is not the right version for your browser, it will show an error message when you try to start a new session using thirtyfour.

Writing Your First Browser Automation Code

Before we begin, you'll need to install Rust. You can do so by using the rustup tool.

Let's start a new project. Open your terminal application and navigate to the directory where you usually put your source code. Then run these commands:

cargo new --bin my-automation-project
cd my-automation-project

You will see a Cargo.toml file and a src/ directory there already.

First, let's edit the Cargo.toml file in your editor (e.g. Visual Studio Code) and add some dependencies:

[dependencies]
thirtyfour = "0.33.0-alpha.2"
tokio = { version = "1", features = ["full"] }

Great! Now let's open src/main.rs and add the following code.

NOTE: Make sure you remove any existing code from main.rs.

Don't worry, we'll go through what it does soon.

/src/main.rs

use std::error::Error;

use thirtyfour::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
    let caps = DesiredCapabilities::chrome();
    let driver = WebDriver::new("http://localhost:9515", caps).await?;
    // Navigate to https://wikipedia.org.
    driver.goto("https://wikipedia.org").await?;
    let elem_form = driver.find(By::Id("search-form")).await?;

    // Find element from element.
    let elem_text = elem_form.find(By::Id("searchInput")).await?;

    // Type in the search terms.
    elem_text.send_keys("selenium").await?;

    // Click the search button.
    let elem_button = elem_form.find(By::Css("button[type='submit']")).await?;
    elem_button.click().await?;

    // Look for header to implicitly wait for the page to load.
    driver.query(By::ClassName("firstHeading")).first().await?;
    assert_eq!(driver.title().await?, "Selenium - Wikipedia");

    // Always explicitly close the browser.
    driver.quit().await?;

    Ok(())
}

Next we need to make sure our webdriver is running.

Open your terminal application and run:

chromedriver

NOTE: This tutorial is currently set up for Chrome. If you'd prefer to run on Firefox instead, this is explained below.

Now open a new tab in your terminal and run your code:

cargo run

If everything worked correctly you should have seen a Chrome browser window open up, navigate to the "Selenium" article on Wikipedia, and then close again.

Running on Firefox

To run the code using Firefox instead, we first need to tell thirtyfour to use the configuration for Firefox. To do this, change the first to lines of your main function to this:

#![allow(unused)]
fn main() {
    let caps = DesiredCapabilities::firefox();
    let driver = WebDriver::new("http://localhost:4444", caps).await?;
}

Now, instead of running chromedriver in your terminal, we'll run geckodriver instead:

geckodriver

And again, in the other tab, run your code again:

cargo run

If everything worked correctly, you should have seen the Wikipedia page open up on Firefox this time.

Congratulations! You successfully automated a web browser.

Understanding Web Browser Automation

So what did this code actually do?

NOTE: This section goes much deeper into how web browser automation works. Feel free to skip ahead and come back when you're ready to dig deeper.

How Thirtyfour Works

Well, the first thing to know is that thirtyfour doesn't talk directly to the Web Browser. It simply fires off commands to the webdriver server as HTTP Requests. The webdriver then talks to the Web Browser and tells it to execute each command, and then returns the response from the Web Browser to thirtyfour. As long as the webdriver is running, thirtyfour can do just about anything a human can do in a web browser.

Explaining The Code

So let's go through the code and see what is going on.

#![allow(unused)]
fn main() {
    let caps = DesiredCapabilities::chrome();
    let driver = WebDriver::new("http://localhost:9515", caps).await?;
}

This is where we actually make the initial connection to the webdriver and start a new session in the web browser. This will open the browser in a new profile (so it won't add anything to your history etc.) and navigate to the default start page.

Capabilities

The way we tell it what browser we want is by using DesiredCapabilities. In this case, we construct a new ChromeCapabilities instance. Each *Capabilities struct will have additional helper methods for setting options like headless mode, proxy, and so on. See the documentation for more details.

You may have heard of selenium. Selenium is simply a proxy that forwards requests to one or more webdriver servers. Using Selenium you can interact with multiple browsers at once. You simply tell Selenium where each of the webdrivers are, and some details of each browser, and then in your code you give thirtyfour the address of the selenium server, not the webdriver, and use the DesiredCapabilities to request a particular browser.

Element Queries

Next, we tell the browser to navigate to Wikipedia:

#![allow(unused)]
fn main() {
    driver.goto("https://wikipedia.org").await?;
}

And then we look for an element on the page:

#![allow(unused)]
fn main() {
    let elem_form = driver.find(By::Id("search-form")).await?;
}

We search for elements using what we call a selector or locator. In this case we are looking for an element with the id of search-form.

If you actually navigate to https://wikipedia.org and open your browser's devtools (F12 in most browsers), then go to the Inspector tab, you will see the raw HTML for the page. In the search box at the top of the inspector, if you type #search-form (the # is how we specify an id) you will see that it highlights the search form element.

This is the container element that contains both the input field and the button itself.

But we want to type into the field, so we need to do another query:

#![allow(unused)]
fn main() {
    let elem_text = elem_form.find(By::Id("searchInput")).await?;
}

And again, if you search in the inspector for #searchInput you get an <input /> element which is the one we want to type into.

Typing Text Into An Element

To type into a field, we just tell thirtyfour to send the keys we want to type:

#![allow(unused)]
fn main() {
    elem_text.send_keys("selenium").await?;
}

This will literally type the text into the input field.

Now we need to find the search button and click it. Finding the element means doing another query:

#![allow(unused)]
fn main() {
    let elem_button = elem_form.find(By::Css("button[type='submit']")).await?;
}

This time we use a CSS selector to locate a <button> element with an attribute type that has the value submit. To learn more about selectors, click here.

Clicking The Button

Next, the call to click() tells thirtyfour to simulate a click on that element:

#![allow(unused)]
fn main() {
    elem_button.click().await?;
}

Dealing With Page Loading Times

The page now starts loading the result of our search on Wikipedia. This brings us to our first issue when automating a web browser. How does our code know when the page has finished loading? If we had a slow internet connection, we might try to find an element on the page only for it to fail because that element hasn't loaded yet.

How do we solve this?

Well, one option is to simply tell our code to wait for a few seconds and then try to find the element we are looking for. But this is brittle and likely to fail. Don't do this. There are cases where you are forced to use this approach, but it should be a last resort. Incidentally, from a website testing perspective, it probably also means a human is going to be unsure of when the page has actually finished loading, and this is an indicator of a poor user experience.

Instead, we usually want to look for an element on the page and wait until that element is visible.

#![allow(unused)]
fn main() {
driver.query(By::ClassName("firstHeading")).first().await?;
assert_eq!(driver.title().await?, "Selenium - Wikipedia");
}

Better Element Queries

To have thirtyfour "wait" until an element has loaded, we use the query() method which provides a "builder" interface for constructing more flexible queries. Again, this uses one of the By selectors, and we tell it to return only the first matching element.

The query() method will poll every half-second until it finds the element. If it cannot find the element within 30 seconds, it will timeout and return an error. The polling time and timeout duration can be changed using WebDriverConfig, or you can also override them for a given query.

The query() method is the recommended way to search for elements. The find() and find_all() methods exist only for compatibility with the webdriver spec. The query() method uses these under the hood, but adds polling and other niceties on top, including giving you more details about your query if an element was not found.

See the ElementQuery documentation for more details on the kinds of queries it supports.

Closing The Browser

Finally we need to close the browser window. If we don't, it will remain open after our application has exited.

#![allow(unused)]
fn main() {
    driver.quit().await?;
}

And this concludes the introduction.

Happy Browser Automation!

Feature Flags

  • rustls-tls: (Default) Use rustls to provide TLS support (via reqwest).
  • native-tls: Use native TLS (via reqwest).
  • component: (Default) Enable the Component derive macro (via thirtyfour_macros).

Element Queries

Basic queries

The find() and find_all() methods in both WebDriver and WebElement provide a simple way to perform direct element queries, returning a result instantly.

However, for many types of queries, these methods are inadequate. For example, there is no polling, and no way to wait for something to show up on a page. If an element doesn't exist at the instant you look for it, you'll get an error.

This isn't helpful for the majority of element queries, so thirtyfour provides a more advanced query interface, called ElementQuery.

ElementQuery

The WebDriver::query() and WebElement::query() methods return an ElementQuery struct.

Using ElementQuery, you can do things like:

#![allow(unused)]
fn main() {
let elem_text =
    driver.query(By::Css("match.this")).or(By::Id("orThis")).first().await?;
}

This will execute both queries once per poll iteration and return the first one that matches.

See ElementQuery for more details.

Waiting For Element Changes

Sometimes you already have a reference to an element, but you want to perform an action that might change the element in some way. One way to do this would be to call WebDriver::query() and poll for the element with its new attributes. But it might be challenging to get the right query, and the query might return a different element that already has the attribute you specified.

If you already have a reference to the element, why not use that to poll for the changes you expect?

The thirtyfour crate provides a way to wait for virtually any desired change on an existing element. It's called ElementWaiter.

ElementWaiter

The WebElement::wait_until() method returns an ElementWaiter struct.

Using ElementWaiter you can do things like this:

#![allow(unused)]
fn main() {
elem.wait_until().displayed().await?;
// You can optionally provide a nicer error message like this.
elem.wait_until().error("Timed out waiting for element to disappear").not_displayed().await?;

elem.wait_until().enabled().await?;
elem.wait_until().clickable().await?;
}

And so on. See the ElementWaiter docs for more details.

Components

Components are a more structured way to automate an element or page.

This approach may seem familiar to anyone who has used a Page Object Model before. However, a Component can wrap any node in the DOM, not just "pages".

It uses smart element resolvers that can lazily resolve elements within the component and cache them for further use. You can also nest components, making them an extremely powerful feature for automating any modern web app.

Example

Given the following HTML structure:

<div id="checkbox-section">
    <label>
        <input type="checkbox" id="checkbox-option-1" />
        Option 1
    </label>

    <label>
        <input type="checkbox" id="checkbox-disabled" disabled />
        Option 2
    </label>

    <label>
        <input type="checkbox" id="checkbox-hidden" style="display: none;" />
        Option 3
    </label>
</div>
#![allow(unused)]
fn main() {
/// This component shows how to wrap a simple web component.
#[derive(Debug, Clone, Component)]
pub struct CheckboxComponent {
    base: WebElement, // This is the <label> element
    #[by(css = "input[type='checkbox']", first)]
    input: ElementResolver<WebElement>, // This is the <input /> element
}

impl CheckboxComponent {
    /// Return true if the checkbox is ticked.
    pub async fn is_ticked(&self) -> WebDriverResult<bool> {
        let elem = self.input.resolve().await?;
        let prop = elem.prop("checked").await?;
        Ok(prop.unwrap_or_default() == "true")
    }

    /// Tick the checkbox if it is clickable and isn't already ticked.
    pub async fn tick(&self) -> WebDriverResult<()> {
        // This checks that the element is present before returning the element.
        // If the element had become stale, this would implicitly re-query the element.
        let elem = self.input.resolve_present().await?;
        if elem.is_clickable().await? && !self.is_ticked().await? {
            elem.click().await?;
            // Now make sure it's ticked.
            assert!(self.is_ticked().await?);
        }

        Ok(())
    }
}

/// This component shows how to nest components inside others.
#[derive(Debug, Clone, Component)]
pub struct CheckboxSectionComponent {
    base: WebElement, // This is the outer <div>
    #[by(tag = "label", allow_empty)]
    boxes: ElementResolver<Vec<CheckboxComponent>>, // ElementResolver works with Components too.
    // Other fields will be initialized with Default::default().
    my_field: bool,
}
}

So how do you construct a Component?

Simple. The Component derive automatically implements From<WebElement>.

#![allow(unused)]
fn main() {
let elem = driver.query(By::Id("checkbox-section")).await?;
let component = CheckboxSectionComponent::from(elem);

// Now you can get the checkbox components easily like this.
let checkboxes = component.boxes.resolve().await?;
for checkbox in checkboxes {
    checkbox.tick().await?;
}
}

This allows you to wrap any component using ElementResolver to resolve elements and nested components easily.

For more details see the documentation for Component and the Component derive macro.

Running against selenium

NOTE: This documentation assumes you are already familiar with Selenium. To learn more about Selenium, visit the documentation

NOTE: To run the selenium example, start selenium server and then run:

cargo run --example selenium_example

Below, you can find my recommended development environment for running selenium tests.

Essentially, you need three main things set up as a minimum:

  1. Selenium standalone running on some server, usually localhost at port 4444.

    For example, http://localhost:4444

  2. The webdriver for your browser somewhere in your PATH, e.g., chromedriver (Chrome) or geckodriver (Firefox)

  3. Your code that imports this library

If you want you can download selenium and the webdriver manually, copy the webdriver to somewhere in your path, then run selenium manually using java -jar selenium.jar.

However, this is a lot of messing around, and you'll need to do it all again any time either selenium or the webdriver gets updated. A better solution is to run both selenium and webdriver in a docker container, following the instructions below.

Setting up Docker and Selenium

To install docker, see https://docs.docker.com/install/ (follow the SERVER section if you're on Linux, then look for the Community Edition)

Once you have docker installed, you can start the selenium server, as follows:

docker run --rm -d -p 4444:4444 -p 5900:5900 --name selenium-server -v /dev/shm:/dev/shm selenium/standalone-chrome:4.1.0-20211123

For more information on running selenium in docker, visit docker-selenium

Using Selenium Manager

The selenium team is working on a project called "Selenium Manager", which is similar to bonigarcia's WebDriverManager but as a CLI. It's written in Rust as a Clap CLI, so we have the benefit of using it as a library as well. To add it to your project, you can add the selenium project as a git dependency in your Cargo.toml. Be sure to specify the branch is "trunk", like so.

[dependencies]
selenium-manager = { git = "https://github.com/SeleniumHQ/selenium", branch = "trunk" }

NOTE: Due to the way the selenium repository is structured, it is quite large. Better integration with selenium-manager, or equivalent functionality, is planned.

Running the tests for thirtyfour

You only need to run the tests if you plan on contributing to the development of thirtyfour. If you just want to use the crate in your own project, you can skip this section.

Make sure selenium is not still running (or anything else that might use port 4444 or port 9515).

To run the tests, you need to have an instance of geckodriver and an instance of chromedriver running in the background, perhaps in separate tabs in your terminal.

Download links for these are here:

  • chromedriver: https://chromedriver.chromium.org/downloads
  • geckodriver: https://github.com/mozilla/geckodriver/releases

In separate terminal tabs, run the following:

  • Tab 1:

    chromedriver
    
  • Tab 2:

    geckodriver
    
  • Tab 3 (navigate to the root of this repository):

    cargo test
    

    NOTE: By default the tests will run in chrome only. If you want to run in firefox, do:

    THIRTYFOUR_BROWSER=firefox cargo test

Frequently Asked Questions

If you have a question not answered here, head over to the discussion page and ask your question there.

Remember to search through existing discussions and issues to check that your question hasn't already been asked/answered.

Why Doesn't The Browser Close On Exit?

Rust does not have async destructors, which means there is no reliable way to execute an async HTTP request on Drop and wait for it to complete. This means you are in charge of closing the browser at the end of your code, via a call to WebDriver::quit() as in the above example.

If you do not call WebDriver::quit() then the browser will stay open until it is either explicitly closed later outside your code, or the session times out.

Further Reading

Thirtyfour Reference Documentation

You can find the Reference Documentation here.

Selenium Documentation

The Selenium Project provides a lot of documentation around Web Browser automation.

Check it out here

The W3C WebDriver V1 Specification

For low level documentation for the W3C WebDriver V1 specification, click here