>_STRYKE-SELENIUM
selenium-python, one stryke pipe away. Browser automation for stryke — chrome / firefox / safari / edge (headless or visible), navigation, element queries (css / xpath / id / name / tag / class / link-text), click / send_keys / clear, attribute / property / CSS reads, JavaScript execution, screenshots (full-page + per-element), window + frame control, and cookie management. A selenium-python-equivalent surface, opt-in as a package so the stryke core binary stays slim. The stryke side is a thin JSON-over-FFI wrapper; the WebDriver client code (thirtyfour + tokio) lives in the libstryke_selenium cdylib.
Install
thirtyfour ships with rustls-tls in the default feature set, so there are no OS-side build deps beyond a working Rust toolchain — no openssl, no Wayland / X11 stack. Just:
s pkg install -g github.com/MenkeTechnologies/stryke-selenium
Pin a specific release:
s pkg install -g github.com/MenkeTechnologies/stryke-selenium@v0.17.0
This fetches the prebuilt tarball for your host triple (aarch64-apple-darwin, x86_64-apple-darwin, x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu), verifies its SHA-256, extracts into ~/.stryke/store/stryke-selenium@<version>/, and registers the cdylib for use Selenium. No cargo, no rustc, no per-target build step on the user's machine.
You also need a WebDriver server (chromedriver / geckodriver / safaridriver) running — see the WebDriver server section below.
Quick start
use Selenium
# launch a browser
Selenium::open(browser => "chrome", headless => 1)
# navigate + read
Selenium::goto("https://example.com")
p "title: " . Selenium::title()
p "url: " . Selenium::current_url()
# find + interact
val $h1 = Selenium::find("h1")
p "h1 text: " . Selenium::text($h1)
# wait + click
val $btn = Selenium::wait_for("button.submit", "css", 10)
Selenium::click($btn)
# screenshot
Selenium::screenshot("/tmp/page.png")
# clean up
Selenium::quit()
API: Selenium::*
Every function lives in the Selenium:: namespace (use Selenium). The last optional argument on most browser-bound calls is $sid — the session id returned by Selenium::open. Omit it to use the active session. Element ops take the element id $eid returned by find / find_all / wait_for. The pure helpers at the end open no browser session at all — they are string parsing / validation that runs without a WebDriver server.
Session lifecycle
Selenium::open(%opts) | browser (chrome/firefox/safari/edge, default chrome), url (WebDriver server URL, default http://localhost:9515), headless (1/0, default 0). Returns the session id. |
Selenium::quit($sid?) | Close one session. |
Selenium::quit_all() | Close every open session. Returns count closed. |
Selenium::sessions() | List of open session ids. |
Selenium::active() | Active session id, or undef. |
Selenium::set_active($sid) | Set the active session. |
Selenium::supported_browsers() | ("chrome", "firefox", "safari", "edge") |
Selenium::locator_strategies() | Every name accepted as $by. |
Navigation & timeouts
Selenium::goto($url, $sid?) · Selenium::get(...) | Navigate. |
Selenium::current_url($sid?) | Current URL. |
Selenium::title($sid?) | Document title. |
Selenium::source($sid?) | Full HTML. |
Selenium::back($sid?) · forward · refresh | History navigation. |
Selenium::set_implicit_wait($s, $sid?) | Server-side implicit-wait timeout. |
Selenium::set_page_load_timeout($s, $sid?) | Page-load timeout. |
Selenium::set_script_timeout($s, $sid?) | Script timeout. |
Selenium::get_timeouts($sid?) | → { script, page_load, implicit } in seconds (a field is undef when unset). |
Selenium::status($sid?) | → { ready, message } WebDriver server status. |
Element queries
Selenium::find($sel, $by="css", $sid?) | Returns one element id, or dies. |
Selenium::find_all($sel, $by="css", $sid?) | Returns a list of ids. |
Selenium::wait_for($sel, $by="css", $timeout=10, $sid?) | Polls every 200 ms until found or timeout. |
Selenium::active_element($sid?) | Element id of document.activeElement (the focused element). |
$by values: css (default), id, name, xpath, tag, class, link_text, partial_link_text. Each has short aliases (css_selector, tag_name, class_name, link, plink) — see Selenium::locator_strategies().
Element ops
Selenium::click($eid) | Click the element. |
Selenium::send_keys($eid, $text) | Type into the element. |
Selenium::clear($eid) | Clear a form field. |
Selenium::text($eid) | Visible text. |
Selenium::attr($eid, $name) | HTML attribute or undef. |
Selenium::prop($eid, $name) | Live DOM property. |
Selenium::css($eid, $name) | Resolved CSS value. |
Selenium::tag($eid) | Lowercase tag name. |
Selenium::class_name($eid) | class attribute as one string, or undef. |
Selenium::value($eid) | Live value DOM property (form fields), or undef. |
Selenium::inner_html($eid) | innerHTML (children markup). |
Selenium::outer_html($eid) | outerHTML (own tag + children). |
Selenium::parent($eid) | Parent node → a fresh element id. |
Selenium::rect($eid) | ($x, $y, $w, $h). |
Selenium::is_displayed($eid) · is_enabled · is_selected | 1 / 0. |
Selenium::is_clickable($eid) | 1/0 — displayed and enabled. |
Selenium::is_present($eid) | 1/0 — handle still attached (not stale). |
Selenium::focus($eid) | Focus the element via DOM focus() (no click). |
Selenium::drop($eid) | Drop the client-side handle. |
JavaScript
Selenium::execute_script($js, $args?, $sid?) | Runs $js with $args as arguments[0..N]. Returns whatever return ... produced, parsed from JSON. To pass a Selenium element, wrap: { __element__ => $eid }. |
Selenium::execute_async_script($js, $args?, $sid?) | Async variant: $js gets a completion callback as its last argument (arguments[arguments.length - 1]); the value passed to it is the return. Blocks until the callback fires or the script timeout elapses. |
Action chains
Low-level pointer/keyboard sequences for cases a plain click/send_keys can't express.
Selenium::action_click($eid, $sid?) | Move pointer to the element's center, then left-click (defeats click interceptors). |
Selenium::action_double_click($eid, $sid?) | Move + double-click. |
Selenium::action_context_click($eid, $sid?) | Move + right-click (context menu). |
Selenium::action_send_keys($text, $sid?) | Type against the focused element via a key-down/key-up sequence. |
Screenshots & scrolling
Selenium::screenshot($path?, $sid?) | $path if given, else \@png_bytes. |
Selenium::element_screenshot($eid, $path?) | Same shape, scoped to one element. |
Selenium::print_page($path, $sid?) | Writes the page as PDF, returns $path. |
Selenium::scroll_to_element($eid) | Scroll element into the viewport. |
Window / frame
Selenium::window_rect($sid?) | ($x, $y, $w, $h). |
Selenium::set_window_rect($x?, $y?, $w?, $h?, $sid?) | Partial accepted. |
Selenium::set_window_size($w, $h, $sid?) | Keeps position. |
Selenium::set_window_position($x, $y, $sid?) | Keeps size. |
Selenium::window_handles($sid?) | Every tab/window handle. |
Selenium::current_window($sid?) | Handle of focused tab. |
Selenium::switch_window($handle, $sid?) | Switch by handle. |
Selenium::new_window($sid?) | Open a new top-level window, switch to it, return its handle. |
Selenium::new_tab($sid?) | Open a new tab, switch to it, return its handle. |
Selenium::close_window($sid?) | Close the current window/tab (closes the session if it's the last one). |
Selenium::set_window_name($name, $sid?) | Set window.name for later switch_to_named_window. |
Selenium::switch_to_named_window($name, $sid?) | Switch by the window.name set above. |
Selenium::switch_frame($eid, $sid?) | Enter iframe by element. |
Selenium::switch_frame_number($n, $sid?) | Enter iframe by zero-based index. |
Selenium::switch_default_content($sid?) | Back to top. |
Selenium::switch_parent_frame($sid?) | One level out. |
Selenium::maximize($sid?) · minimize · fullscreen | Window state. |
Alerts
Selenium::accept_alert($sid?) | Click OK on alert/confirm/prompt. |
Selenium::dismiss_alert($sid?) | Click Cancel. |
Selenium::alert_text($sid?) | Text of the current alert. |
Selenium::send_alert_text($text, $sid?) | Type into a JS prompt. |
Cookies
Selenium::cookies($sid?) | Arrayref of cookie hashes. |
Selenium::get_named_cookie($name, $sid?) | One cookie hash by name, or dies if absent. |
Selenium::add_cookie(%fields) | name + value required; path, domain, secure, http_only, same_site, expiry optional. |
Selenium::delete_cookie($name, $sid?) | Delete one cookie. |
Selenium::delete_all_cookies($sid?) | Delete every cookie. |
Pure helpers (no browser)
These open no session — string parsing / validation that runs without a WebDriver server:
Selenium::parse_locator("css=.btn") | → { strategy, value } — splits strategy=value, canonicalizes the strategy for find (bare value → css). |
Selenium::build_locator($strategy, $value) | → { locator, strategy } — inverse of parse_locator; rejects unknowns. |
Selenium::valid_locator_strategy($s) | → { strategy, valid, canonical }. |
Selenium::locator_to_w3c($strategy, $value?) | → { using, value, strategy } — the W3C WebDriver protocol pair; id/name/class collapse to a css selector. |
Selenium::w3c_to_locator($using, $value) | → { strategy, value, locator } — inverse for the five W3C using strategies. |
Selenium::key_code($key) | → { key, code_point, codepoint, char } — WebDriver special-key PUA code point (Enter→U+E007, F1–F12, aliases). |
Selenium::key_name(%opts) | → { key, code_point, codepoint, char } — PUA code point → canonical key name; inverse of key_code. |
Selenium::parse_cookie("a=b; Path=/; Secure") | → { name, value, domain, path, secure, http_only, same_site, expires } — feeds add_cookie. |
Selenium::build_cookie(%opts) | → Set-Cookie string — inverse of parse_cookie; truthy secure/http_only become bare flags. |
Selenium::cookie_domain_matches($cookie_domain, $host) | → { cookie_domain, host, matches } — RFC 6265 §5.1.3 domain matching. |
Selenium::cookie_path_matches($cookie_path, $request_path) | → { cookie_path, request_path, matches } — RFC 6265 §5.1.4 path matching. |
Selenium::css_escape($value) | → escaped string — CSSOM serialize-an-identifier (browser CSS.escape). |
Selenium::css_unescape($escaped) | → decoded string — inverse of css_escape (CSS Syntax §4.3.7). |
Selenium::css_escape_string($value) | → quoted string — escape + double-quote a CSS attribute-selector value ([attr="…"]). |
Selenium::build_css_selector(%parts) | → selector string — compose tag/id/classes/attributes into a CSS selector. |
Selenium::parse_css_selector($selector) | → { tag, id, classes:[…], attributes:{…} } — inverse of build_css_selector. |
Selenium::build_xpath(%parts) | → xpath string — compose tag/id/classes/attributes/text/contains_text into a // locator. |
Selenium::xpath_literal($value) | → literal string — quote an arbitrary string as an XPath 1.0 literal; uses concat() when it contains both ' and ". |
The same list, with full per-argument notes, lives in the README API reference.
WebDriver server
stryke-selenium is a client. You launch the WebDriver server yourself. One-liners per browser:
# Chrome — default. Matches Selenium::open() with no url arg. brew install --cask chromedriver chromedriver --port=9515 & # Firefox — pass url => "http://localhost:4444" to Selenium::open. brew install geckodriver geckodriver --port 4444 & # Safari — macOS only; one-time enable: safaridriver --enable safaridriver -p 4444 & # Edge — install msedgedriver matching your Edge version. msedgedriver --port=9515 & # Selenium Grid 4 (all browsers via one server): brew install selenium-server selenium-server standalone --port 4444 &
To verify the install + FFI roundtrip without launching a browser:
selenium-test # installed at ~/.stryke/bin/selenium-test by `s pkg install -g`
Why a cdylib, not a sidecar binary
Two competing models for stryke connectors:
- Sidecar binary — the connector ships a regular Rust binary; the
.stkwrapperfork(2) + exec(2)s it per call, parsing JSON over a pipe. Simple, ABI-stable, isolates crashes — but every call pays a process-creation cost plus any per-call session-init cost the binary has (Enigo handshake, WebDriver session resume, …). - cdylib (this package) — the connector ships a shared library; stryke
dlopens it once per process and registers each export as a stryke-callable function. Calls are direct function calls with nofork/exec— just JSON-in / JSON-out across the FFI boundary.
For Selenium the second model is mandatory: a WebDriver session is an HTTP keep-alive pool plus a server-side browser process, both of which take hundreds of milliseconds to set up. Paying that per Selenium::* call would make any non-trivial script unusable. The cdylib holds the WebDriver handle in a process-global OnceCell<Mutex<HashMap<u64, WebDriver>>> so one Selenium::open serves the whole script.
thirtyfour is async (tokio), but the stryke FFI is sync. The cdylib bridges via a single process-global tokio::runtime::Runtime built on first FFI entry; every handler is runtime.block_on(async { … }). A fresh runtime per call would be ~1 ms of pure tokio bring-up overhead — fine in isolation, ruinous across thousands of Selenium::* calls.
FFI protocol
Every selenium__* export takes a JSON-encoded args dict and returns a JSON-encoded result. The lib/Selenium.stk wrapper builds the args, calls the FFI symbol, and parses the return. Errors come back as {"error": "<message>"} — the wrapper recognizes this shape and dies with a descriptive message.
selenium__open | {browser, url, headless} → {session} |
selenium__sessions | {} → {sessions: […], active: u64?} |
selenium__find | {session?, by, selector} → {element} |
selenium__find_all | {session?, by, selector} → {elements: […]} |
selenium__wait_for | {session?, by, selector, timeout} → {element} |
selenium__element_text | {element} → {text} |
selenium__element_attr | {element, name} → {value: string?} |
selenium__execute_script | {session?, script, args: […]} → {value} |
selenium__screenshot | {session?, output?} → {path} or {png: [u8, …]} |
selenium__cookies | {session?} → {cookies: […]} |
selenium__supported_browsers | {} → […] |
selenium__locator_strategies | {} → […] |
To pass a Selenium WebElement as a JS arg, wrap it as { __element__ => $eid }. The cdylib detects this shape and unmarshals it into a real DOM element on the JS side. Mirrors selenium-python's execute_script(…, web_element) convention.
Examples
| open chrome, get title, quit | s examples/selenium_basic.stk |
| same, no visible window | s examples/selenium_headless.stk |
| find input, send_keys, submit | s examples/selenium_form_fill.stk |
| full-page + per-element capture | s examples/selenium_screenshot.stk |
| wait_for with 10s timeout | s examples/selenium_wait.stk |
| add / list / delete cookies | s examples/selenium_cookies.stk |
| execute_script with WebElement args | s examples/selenium_js.stk |
| multi-tab switch | s examples/selenium_windows.stk |
| two browsers in parallel | s examples/selenium_multi_session.stk |
Layout
stryke-selenium/ ├── stryke.toml # stryke package manifest with [ffi] table ├── Cargo.toml # stryke_selenium cdylib crate manifest ├── src/ │ ├── lib.rs # selenium__* FFI exports + ffi_call wrapper │ ├── common.rs # tokio runtime + session + element registries │ ├── driver.rs # open / quit / navigation / locator parsing │ ├── element.rs # find / wait_for / click / text / attr / ... │ ├── script.rs # execute_script / execute_async_script with WebElement arg unmarshaling │ ├── actions.rs # action-chain pointer/keyboard sequences (click / double / context / send_keys) │ ├── capture.rs # page + per-element screenshots │ └── window.rs # window rects / handles / new-window / frames / cookies ├── lib/Selenium.stk # stryke wrappers (JSON args → FFI symbol → JSON return) ├── bin/selenium-test.stk # installable smoke-test launcher ├── examples/ # runnable .stk demos ├── t/test_selenium.stk # plumbing tests (permission-free FFI surface) ├── Makefile # `make install` builds + installs └── docs/ # this site (GitHub Pages)
Sibling packages
Part of the stryke package family. Browse the others via the MenkeTechnologiesMeta umbrella repo:
- stryke-arrow — Apache Arrow / Parquet / Feather
- stryke-aws — S3, DynamoDB, SQS, Lambda, STS
- stryke-azure — Blob, Queues, Cosmos DB, Key Vault, Entra
- stryke-clickhouse — ClickHouse
- stryke-demo — live demos for every connector
- stryke-docker — Docker daemon API
- stryke-duckdb — embedded DuckDB SQL engine
- stryke-email — email + campaign client
- stryke-fleet — parallel expect/PTY automation
- stryke-gcp — Cloud Storage, Pub/Sub, Secret Manager, BigQuery, Firestore
- stryke-grpc — reflection-based gRPC client
- stryke-gui — mouse / keyboard / screen / clipboard automation
- stryke-k8s — Kubernetes
- stryke-kafka — Apache Kafka
- stryke-mcpd — MCP servers without a runtime
- stryke-mongo — MongoDB
- stryke-mssql — Microsoft SQL Server
- stryke-mysql — MySQL / MariaDB
- stryke-neo4j — Neo4j graph (Bolt)
- stryke-office — Office docs / PDF / images
- stryke-parquet — Parquet file inspector
- stryke-polars — Polars + ndarray + linalg + FFT
- stryke-postgres — PostgreSQL
- stryke-redis — Redis / Valkey
- stryke-scrape — web scraping / crawling
- stryke-scylla — ScyllaDB / Cassandra
- stryke-search — Elasticsearch / OpenSearch
- stryke-spark — Spark Connect (no JVM)
- stryke-utils — pure-stryke utility belt
- stryke-zmq — ZeroMQ messaging