Etaoin Documentation

repository·master·Indexed 21 days ago

https://github.com/clj-commons/etaoin

A pure Clojure implementation of the WebDriver protocol for browser automation and frontend testing. Etaoin provides a lightweight, Selenium-free way to control browsers including Chrome, Firefox, Safari, and Edge. It supports W3C WebDriver specifications, CSS and XPath selectors, Shadow DOM interaction, and human-like typing emulation. Compatible with Clojure and Babashka.

Tokens
8.3K
Snippets
37
Records
45
Agent score
76%

What's inside Etaoin

  1. Overview of Etaoin

    master

    Etaoin is a pure Clojure implementation of the Webdriver protocol. It is designed to be a lightweight, fast, and Selenium-free alternative for automating browsers, testing frontend behavior, and simulating human actions.

    Key features include:

    • Browser Support: Currently supports Chrome, Firefox, Safari, and Edge.
    • Connection Modes: Can connect to a remote WebDriver process or launch one automatically.
    • Human-like Simulation: Capable of imitating human behavior such as delays and typos.
    • Lightweight: Minimal dependencies and a compact structure centered around a single main namespace.
  2. Using CSS and XPath selectors

    master

    Etaoin supports CSS and XPath selectors. By default, the driver's :locator is set to "xpath".

    Selector Types

    1. Keywords: Any keyword (except :active) is treated as an HTML ID attribute (e.g., :uname becomes id="uname").
    2. Strings:
      • If the driver is in XPath mode, strings are treated as XPath.
      • If the driver is in CSS mode, strings are treated as CSS.
    3. Maps: Explicitly define the syntax using :xpath or :css keys.

    Switching Locators

    Use e/use-css or e/use-xpath to get a new driver instance configured with a different default locator.

    ;; Using a map with explicit CSS
    (e/fill driver {:css "input#uname"} "value")
    
    ;; Switching the whole driver to CSS mode
    (def driver-css (e/use-css driver))
    (e/fill driver-css "input#uname" "value")
    (e/fill driver {:css "input#uname[name='username']"} "CSS can be tricky, too")
  3. How wait-* functions and wait-predicate work

    master

    In recent versions, wait-predicate and all functions derived from it (e.g., wait-*) measure their :timeout against real elapsed time. Previously, the timeout was calculated by subtracting the :interval once per attempt and ignoring the time spent executing the predicate itself, which often caused waits to run significantly longer than requested.

    Key changes to note:

    • The predicate is now always called at least once, regardless of the :timeout value.
    • The thrown :etaoin/timeout map now includes an :elapsed-ms key.
    • The :times value in the timeout map is no longer a fixed function of :timeout and :interval; it now depends on how long the predicate takes to execute.
    • Warning: If you previously relied on waits succeeding because they were accidentally granted extra time, they may now time out. If you encounter new timeouts, increase your :timeout value.
    • The options :time-rest and :times are internal recursion state and should not be passed as inputs.
    ;; Example of a wait with timeout and interval
    (wait-visible driver {:timeout 10 :interval 0.5})
  4. Understand the driver instance change (v0.4.0 breaking change)

    master

    In v0.4.0, the driver instance changed from being an atom to being a map.

    Impact: If your code uses swap! or similar atom-manipulation functions on the driver instance, you must refactor your code. All internal functions that previously modified the driver atom now return a new version of the map instead.

  5. Interacting with Shadow DOM

    master

    Elements inside a Shadow DOM are hidden from standard query calls. You must first locate the shadow root host and then query within that root.

    Workflow

    1. Find the host element in the normal DOM.
    2. Retrieve the shadow root.
    3. Query from that shadow root.

    API Methods

    • e/get-element-shadow-root driver <query>: Returns the shadow root ID for a matching query.
    • e/get-element-shadow-root-el driver <element-id>: Returns the shadow root ID for a specific element.
    • e/has-shadow-root? driver <query>: Checks if an element is a shadow root host.
    • e/query-from-shadow-root driver <host-query> <shadow-query>: Executes a shadow query starting from a host found via <host-query>. Note: Use CSS for the shadow-query as browsers currently lack XPath support for Shadow DOM.
    • e/query-from-shadow-root-el driver <shadow-root-id> <shadow-query>: Executes a shadow query starting from a specific shadow root ID.

    Example

    ;; Querying using the host query and a CSS shadow query
    (e/query-from-shadow-root driver {:id "shadow-root-host"} {:css "#in-shadow"})
    (e/get-element-shadow-root driver {:id "shadow-root-host"})
  6. Querying elements with Map Syntax

    master

    Etaoin allows you to build XPath expressions using Clojure maps. This is often more readable and robust than raw strings.

    Map Syntax Rules

    • :tag: The HTML tag name (defaults to *).
    • Any other key: Represents an attribute and its value (e.g., :id "uname").
    • :fn/*: A prefix for supported query functions.

    Supported Query Functions (:fn/*)

    • :fn/index <int>: Selects the n-th element (1-based).
    • :fn/text <string>: Matches exact text within the element.
    • :fn/has-text <string>: Matches if the element includes the specified text.
    • :fn/has-string <string>: Matches if the element's concatenated descendant text contains the string.
    • :fn/has-class <string>: Matches if the class attribute includes the string.
    • :fn/has-classes <vector>: Matches if the class attribute includes ALL specified strings.
    • :fn/link <string>: Matches if the href attribute contains the string.
    • :fn/enabled <bool>: Matches if the element is enabled/disabled.
    • :fn/disabled <bool>: Matches if the element is disabled/enabled.

    Examples

    ;; Find the 1st div
    (e/query driver {:tag :div :fn/index 1})
    
    ;; Find a button with exact text
    (e/query driver {:tag :button :fn/text "Submit Form"})
    
    ;; Find an element including a specific class
    (e/query driver {:tag :span :fn/has-class "class1"})
    
    ;; Find an element including all specified classes
    (e/query driver {:fn/has-classes [:class2 :class3]})
    (e/query driver {:tag :button :fn/text "Submit Form"})
  7. Install Etaoin for Clojure or Babashka

    master

    To use Etaoin, you must add the library as a dependency and install the appropriate WebDrivers for the browsers you intend to control.

    Clojure Users

    Add to your project.clj:

    [etaoin "1.1.43"]

    Or to your deps.edn:

    etaoin/etaoin {:mvn/version "1.1.43"}

    Babashka Users

    Add to your bb.edn:

    etaoin/etaoin {:mvn/version "1.1.43"}

    Tip: Babashka uses timbre for logging. To reduce noise, set the log level to :info:

    (require '[taoensso.timbre :as timbre])
    (timbre/set-level! :info)
  8. Capture screenshots

    master

    Etaoin provides several ways to capture visual state:

    • screenshot: Saves the entire visible page to a PNG file.
    • screenshot-element: (Chrome/Firefox only) Captures a specific element.
    • with-screenshots: A macro that takes a screenshot after every form executed within the block. Files are named using the pattern <webdriver-name>-<timestamp>.png.
    ;; Full page screenshot
    (e/screenshot driver "path/to/image.png")
    
    ;; Element screenshot (Chrome/Firefox)
    (e/screenshot-element driver {:tag :form} "element.png")
    
    ;; Automatic screenshots after every step
    (e/with-screenshots driver "target/dir"
      (e/fill driver :uname "et")
      (e/fill driver :uname "ao"))
  9. Select an option from a dropdown

    master

    You can select an <option> from a <select> element using the click function by providing a selector that includes the target value or text.

    Safari Quirk: Safari may require you to click the select element itself before clicking the specific option.

    Etaoin provides a select convenience function that handles the Safari quirk automatically and selects the first option matching the provided text.

    ;; Click by value
    (e/click driver [{:id :dropdown} {:value "o4"}])
    
    ;; Click by text
    (e/click driver [{:id :dropdown} {:fn/text "bar three"}])
    
    ;; Using the convenience function (handles Safari quirk)
    (e/select driver :dropdown "bar")
  10. Preview documentation with Cljdoc

    master

    This experimental feature allows you to preview how documentation will look on cljdoc.org. You must push your changes to GitHub for a full preview (including links and images).

    bb cljdoc-preview start
    bb cljdoc-preview ingest
    bb cljdoc-preview view
    bb cljdoc-preview stop
  11. Execute JavaScript in the browser

    master

    Use js-execute to run synchronous JavaScript. You can pass arguments to the script, which are accessible via the arguments array in JS. To return data to Clojure, include a return statement in your script; Etaoin automatically converts the resulting JSON to EDN.

    For asynchronous scripts (e.g., those using setTimeout), use js-async. You must call the callback function provided by WebDriver as the last argument in your script to signal completion.

    ;; Synchronous execution with arguments and return value
    (e/js-execute driver "return {foo: arguments[0].val}" {:val "hello"})
    
    ;; Asynchronous execution
    (e/js-async driver 
      "var callback = arguments[arguments.length-1];
       setTimeout(function() { callback('done'); }, 1000);")