thirtyfour

repository·main·Indexed 23 days ago

https://github.com/stevepryde/thirtyfour

A Selenium/WebDriver library for Rust providing a high-level, asynchronous API for automated website UI testing. It supports W3C WebDriver, Chrome DevTools Protocol (CDP), and WebDriver BiDi. The library integrates with the tokio runtime and provides features for element querying, browser lifecycle management, and network interception via BiDi.

Tokens
55.7K
Snippets
117
Records
207
Agent score
76%

What's inside thirtyfour

  1. Available automation recipes

    main

    The following recipe categories are available to help with common automation tasks:

    • Forms And Page Content: Covers login flows, search functionality, HTML modals, and table/list assertions.
    • Frames, Shadow DOM, And Files: Covers iframe switching, shadow-root queries, and file uploads.
    • Failure Artifacts And Logs: Covers capturing screenshots on failure and accessing browser/driver logs.
    • CDP And BiDi: Covers typed Chromium cache commands and cross-browser BiDi event subscriptions.
  2. Implement the standard interaction flow

    main

    A reliable interaction follows a specific sequence to ensure the element is ready and the action was successful:

    1. Query: Locate the element using a polling query.
    2. Cardinality: Specify if you expect one or many elements.
    3. Readiness: Ensure the element meets required filters (e.g., being displayed or enabled).
    4. Action: Perform the interaction (e.g., .click()).
    5. Outcome: Assert a user-visible or protocol-visible change to confirm the action worked.

    Note on 'Clickable': In thirtyfour, an element is considered 'clickable' if it is both displayed and enabled, but this is not a complete guarantee of interactability.

  3. Compare CDP vs WebDriver BiDi

    main

    Decide between CDP and BiDi based on your requirements:

    RequirementUse
    Cross-browser support (Chrome and Firefox)BiDi
    Rich Chromium-only features (Full Network, Fetch, DOM)CDP
    W3C-standard, future-proof bidirectional protocolBiDi
    Accessing Network.*, Fetch.*, Runtime.*, etc.CDP
    Resolving a WebElement to a CDP RemoteObjectIdCDP

    Both can be used in the same session. CDP is on by default; BiDi requires opting in via a capability and a feature flag.

  4. Difference between `query()` and `find()` / `find_all()`

    main

    The find() and find_all() methods are one-shot lookups that mirror the W3C WebDriver specification. They do not perform polling, do not support filters, and provide minimal error information if no match is found.

    Recommendation: Use query() for almost all automation tasks. query() handles slow page loads, missing elements, and flickering DOMs much more gracefully than the one-shot find() methods.

  5. Selector Best Practices for AI-Friendly Automation

    main

    To ensure robust and AI-friendly browser automation, follow these selector strategies:

    1. Prioritize Stable Selectors: Use By::Testid as the primary method for locating elements. This provides a stable contract between the application and the automation script.
    2. Use Meaningful Queries: When using text-based queries, ensure they are descriptive and unique. Be aware that text matching can be brittle; use it when meaningful, but rely on other methods when possible.
    3. CSS Escape Hatch: For custom test attributes that cannot be easily targeted via standard selectors, use CSS selectors as an escape hatch.
    4. XPath for Complex Targets: Reserve XPath for targets that cannot be expressed via CSS.
    5. Avoid One-Shot find() in Flows: For standard user flows (like searching), prefer query-based flows over calling find() or find_all() as one-shot APIs, unless the specific use case requires a single, immediate lookup.
  6. How Components work in thirtyfour

    main

    Components allow you to wrap pieces of UI (buttons, forms, pages) in a Rust struct to avoid brittle, repetitive selector logic. This is similar to the Page Object Model.

    Key characteristics:

    • A Component is a struct that derives the Component macro.
    • It must contain exactly one base field of type WebElement which represents the outer element the component wraps.
    • Resolver fields (marked with #[by(...)]) are ElementResolver types that query the DOM starting from the base element.
    • Resolvers are lazy: they don't query until .resolve() is called, and they cache the result to avoid redundant WebDriver calls.
    • Scoping: Queries are scoped to the component's subtree. When using XPath, use .// to stay within the component; using // will search from the document root.
    use thirtyfour::prelude::*;
    
    #[derive(Debug, Clone, Component)]
    pub struct SearchForm {
        base: WebElement,                              // The <form> itself.
        #[by(id = "search-input")]
        input: ElementResolver<WebElement>,            // The <input>.
        #[by(testid = "search-submit", description = "search submit button")]
        submit: ElementResolver<WebElement>,           // The <button>.
    }
    
    impl SearchForm {
        pub async fn search(&self, term: &str) -> WebDriverResult<()> {
            self.input.resolve().await?.send_keys(term).await?;
            self.submit.resolve().await?.click().await?;
            Ok(())
        }
    }
    
    // Usage:
    let form_el = driver.query(By::Id("search-form")).single().await?;
    let form: SearchForm = form_el.into();         // From<WebElement> is derived.
    form.search("Selenium").await?;
  7. Perform element queries with polling

    main

    The recommended way to find elements is using the query() method. Unlike the lower-level find() or find_all() methods (which are one-shot operations), query() provides a builder interface that automatically polls for the element to appear.

    By default, query() polls every half-second for up to 20 seconds. If the element does not appear within the timeout, it returns an error including the selector and any description provided via .desc().

    Common query terminators:

    • .single(): Asserts that exactly one element matches the selector.
    • .first(): Selects the first element among several matches.
    let elem_form = driver
        .query(By::Id("search-form"))
        .desc("Wikipedia search form")
        .single()
        .await?;
  8. How ElementWaiter and ElementQuery differ

    main

    Choosing between ElementQuery and ElementWaiter depends on whether you are searching for an element or waiting for an existing one to change:

    • Use ElementQuery when you are looking for an element on the page. It repeatedly evaluates selectors and filters. Use query(...).wait_until_gone() if you want to wait for an element matching a specific query to disappear (this re-runs the selector/filters to ensure no replacement element matches).
    • Use ElementWaiter when you already have a resolved WebElement and want to watch it reach a specific state (e.g., becoming visible, clickable, or stale). Use elem.wait_until().stale() if you want to wait for that specific remote element ID to detach from the DOM. This is useful after a click to ensure the element you acted on is gone, even if a new element matching the same selector is immediately rendered.
    • Use not_exists() when you want to poll a query as a boolean rather than waiting for a timeout error.
  9. Use scoped locators for nested elements

    main

    To scope lookups (similar to Playwright's locator chaining), query from a container WebElement.

    Warning: Unlike Playwright Locator objects, an ElementQuery terminator resolves to a concrete WebElement. If the page re-renders, the held element may become stale. You must run the query again to re-resolve the element or use an ElementResolver within a Component for automatic stale-element recovery.

    # use thirtyfour::prelude::*;
    # async fn example(driver: &WebDriver) -> WebDriverResult<()> {
    let dialog = driver
        .query(By::Testid("delete-dialog"))
        .and_displayed()
        .desc("delete confirmation dialog")
        .single()
        .await?;
    
    dialog
        .query(By::Testid("confirm-delete"))
        .and_clickable()
        .desc("confirm delete button")
        .single()
        .await?
        .click()
        .await?;
    # Ok(())
    # }
  10. Handle stale elements with caching strategies

    main

    Resolvers in thirtyfour cache the resolved value to make repeated calls efficient. However, if the DOM changes (re-renders, SPA route changes, etc.), the cached WebElement may become stale.

    Use these strategies to manage staleness:

    • For frequently changing elements: Use the resolve_present!(field) macro. This checks if the cached value is still attached to the DOM and re-queries if it is not. This is the safe default.
    • For stable elements: Use the resolve!(field) macro for better performance if the element is guaranteed to persist.
    • Manual invalidation: If you know the DOM has changed, call .invalidate() or .resolve_force() on the resolver to clear the cache.