Skip to main content
MustCodeAl/tauri-plugin-sbrs-automation

Drive a Tauri app's webview from seleniumbase-rs tests on macOS, Windows and Linux (WebDriver inside the app)

00
Early: 0.1 Tauri 3.0.0-alpha.4 W3C WebDriver macOS: verified Windows: not run yet Linux: not run yet

Test a Tauri app’s real window with seleniumbase-rs (BaseCase and the sbrs CLI) on macOS, Windows and Linux.

Tauri apps use the system webview. Windows (WebView2) and Linux (WebKitGTK) have WebDriver drivers, but macOS (WKWebView) has none. This plugin runs a small W3C WebDriver endpoint inside your app, so the same tests drive the app everywhere.

Key facts
  • Endpoint: http://127.0.0.1:4445/{token}/
  • On in debug builds, or with SBRS_AUTOMATION=1
  • Token: SBRS_AUTOMATION_TOKEN, or a random one printed at start
  • One window, main, and one session at a time
  • Input is simulated in the page
  • Screenshots on macOS only
API reference

Every route with its fields, replies and errors.

Note

Early (0.1, Tauri 3 alpha). It serves the core of WebDriver: sessions, timeouts, navigation, finding elements (CSS, XPath, link text, tag name), clicking, typing, reading text, attributes, properties and state, running scripts (sync and async), the window’s rectangle (read, move, resize, maximize, minimize, full screen), user prompts (alert, confirm, prompt), same-origin frames, actions (mouse and keys) and, on macOS, screenshots. See Supported commands. Clicks, typing, actions and dialogs are simulated in the page with DOM events and page script, not OS-level input, so the mouse and keyboard are never used.

Check

Verified on a real app (macOS, Tauri 3.0.0-alpha.4 with the wry runtime): the tauri-profile-manager example of seleniumbase-rs is driven by BaseCase (thirtyfour 0.37) in an end-to-end test.

  • 2026-10-15: the test starts a session, reads the title, finds elements by CSS selector, waits for them to be displayed and enabled, clicks, clears and types into inputs, reads and asserts text, reads and resizes the window, and ends the session; sync and async scripts were run on the same app with raw WebDriver requests.
  • 2026-10-15, then: the same test answered a confirm and a prompt (answer set before, and the default dismiss), entered an <iframe srcdoc> (finding, a script, a click and its alert inside) and was refused a sandboxed frame, double-clicked, hovered and pressed Space and Enter through W3C actions, and took a page screenshot (1800 x 1400 for a 900 x 700 window on a Retina display) and an element screenshot.
  • 2026-10-16: it was refused accepting a confirm that had been dismissed when it opened, saw an alert leave the answer to the confirm after it and cleared the two in order, and hovered and took the element screenshot of a paragraph far below the view on a page with scroll-behavior: smooth.
Warning

Not run on a real app yet: Windows and Linux; the XPath, link-text and tag-name strategies, which no test covers; DELETE /actions, entering a frame by index and POST /frame/parent, whose requests the Rust unit tests check against a fake page script; right clicks, drags and Backspace, whose page side is checked only by the tests of src/bridge.js in a fake page under Node.js (tests/bridge.test.mjs, which cargo test runs and which need node on PATH), not in a webview; and element screenshots of a zoomed page, checked only by a unit test of the conversion to the web view’s points.

Set up your app#

Add the plugin

Add the plugin, best behind a feature so release builds never have it:

src-tauri/Cargo.toml

Register it

Register it:

The app's builder

Allow its command

Allow its command. With the plugin behind a feature, put the permission in a capability of its own, src-tauri/capabilities/e2e/automation.json:

src-taurithe Rust side of the app
Cargo.tomlthe e2e feature and the optional plugin
build.rsreads capabilities/e2e only with the feature
capabilities
default.jsoncannot name sbrs-automation:default next to an optional plugin
e2e
automation.jsongrants sbrs-automation:default to the main window
src-tauri/capabilities/e2e/automation.json

and let build.rs read that folder only with the feature. A capability that names sbrs-automation:default while the plugin is not compiled in fails the build (Permission sbrs-automation:default not found), so it cannot go in default.json next to an optional plugin:

src-tauri/build.rs

If the plugin is always compiled in, "sbrs-automation:default" can simply join the permissions of capabilities/default.json.

Name the runtime (Tauri 3)

On Tauri 3, the app must name its runtime, or it stops at start with RuntimeNotConfigured: depend on tauri-runtime-wry (the same alpha) and call .runtime(tauri_runtime_wry::Wry::default()) on the builder.

Ask a coding agent to set it upOpen in Cursor

Add tauri-plugin-sbrs-automation (git https://github.com/MustCodeAl/tauri-plugin-sbrs-automation) to this Tauri app behind a Cargo feature named e2e, as https://mustcodeal-seleniumbase-rs.docs7.io/ecosystem/tauri describes: make the dependency optional, register the plugin on the builder only with the feature, grant sbrs-automation:default in src-tauri/capabilities/e2e/automation.json, let build.rs read the capabilities folder recursively only when CARGO_FEATURE_E2E is set, and on Tauri 3 depend on tauri-runtime-wry and call .runtime(tauri_runtime_wry::Wry::default()) on the builder.

Run a test#

Start the app with a token of your choice (or read the random one it prints):

Terminal

Then point BaseCase at it. The URL ends with /: WebDriver clients join each command path (session, …) to the server URL as a relative URL, so without the slash the token is replaced and every request is refused with the URL does not start with the token.

A test

Any WebDriver client works the same way with http://127.0.0.1:4445/secret/ as its server URL. A new session waits until the app’s window has loaded its page, so a test can start one as soon as the endpoint answers GET /secret/status. For a test that starts the app itself, see examples/tauri-profile-manager/src-tauri/tests/e2e_automation.rs in seleniumbase-rs.

Settings#

The plugin reads these environment variables once, when the app starts.

SBRS_AUTOMATIONstring#

0, false or off turn the plugin off; any other value, such as 1, turns it on. Unset or empty, the build decides. Off, the plugin injects no page script and serves no endpoint.

Default: "on in debug builds, off in release"

SBRS_AUTOMATION_PORTinteger#

The port on 127.0.0.1. If the port is taken (a second instance, a Selenium container), the app runs without the endpoint and says so on stderr. A value that is not a port number counts as unset.

Default: "4445"

SBRS_AUTOMATION_TOKENstring#

The first segment of every path. Unset or empty: a random token of 32 hexadecimal digits, printed when the app starts, in the endpoint’s URL.

Default: "random"

SBRS_AUTOMATION_TRACE_WINDOWstring#

Any non-empty value makes each window command print the window’s state on stderr before it asks for a change, after, and when it answers: what Tauri reports and, on macOS, what the NSWindow says (style mask, isZoomed, frame, content view, screen). For finding out why a resize did not stick.

Default: "off"

Supported commands#

Every path starts with /<token>; session commands with /session/{id}. The API reference gives each command’s fields, replies and errors, with request and response examples.

CommandEndpointNotes
StatusGET /statusNo session needed.
New and delete sessionPOST /session, DELETE /session/{id}One session at a time: a new one replaces the last. A new session starts in the top page, with no dialog answer or input held from the last one.
TimeoutsGET, POST /timeoutsscript, pageLoad, implicit (finding looks again until it is over).
NavigationPOST /url, GET /url, GET /title, POST /back, /forward, /refreshWaits for the new page to load.
Page sourceGET /sourceOf the current frame.
WindowGET /window, /window/handles, POST /window, GET/POST /window/rect, POST /window/maximize, /minimize, /fullscreenOne window, main. A change waits until the window has it (normally well under a second; each state change is waited for up to three seconds, the final rectangle up to two more), and the answer is the window’s rectangle then, which can differ from the one asked for: the system keeps a window below the menu bar and may refuse a size.
FramesPOST /frame, POST /frame/parentSame-origin frames only.
Find elementsPOST /element, /elements, /element/{id}/element, /element/{id}/elements, GET /element/activeCSS, XPath, link text, partial link text, tag name.
Element stateGET /element/{id}/text, /name, /rect, /enabled, /selected, /displayed, /attribute/{name}, /property/{name}, /css/{name}
Element interactionPOST /element/{id}/click, /clear, /valueDOM events.
ScriptsPOST /execute/sync, /execute/asyncRun in the current frame.
ActionsPOST /actions, DELETE /actionsMouse, keys and pauses.
User promptsGET /alert/text, POST /alert/accept, /alert/dismiss, /alert/textSimulated: answered when they open.
ScreenshotsGET /screenshot, GET /element/{id}/screenshotmacOS only; PNG.
Info

Not served: cookies, shadow roots, and opening or closing windows (unknown command); screenshots on Windows and Linux, wheel input, pen and touch pointers (unsupported operation); cross-origin frames (no such frame). The endpoint drives one window, main (or, without one, another window of the app). Cookies are left out on purpose: on macOS the app’s page is tauri://localhost, where WKWebView keeps no cookies at all (document.cookie reads back empty after a write).

Details#

Warning

Dialogs are answered when they open. A test that should accept a confirm accepts first, then clicks; the usual WebDriver order, click and then accept, gets unsupported operation. See User prompts.

Changing the page script#

The page script is written in TypeScript in src/bridge.ts. src/bridge.js is generated from it and is what the plugin embeds, so apps that use the plugin build without Node or Bun.

Edit the source

Change src/bridge.ts, then run bun install once.

Generate the script

Run bun run bridge. It builds bridge.js and records the SHA-256 of bridge.ts on its first line. Commit both files. bunx tsc --noEmit type-checks the source.

Note

cargo test fails, naming this command, when bridge.js is older than bridge.ts.

How it works#

The server turns each WebDriver command into an operation of an injected script (src/bridge.js), which runs it in the page and sends the result back through Tauri’s IPC. The window commands and screenshots ask the native window instead: the page cannot tell the window’s outer size (WKWebView reports it as 0), and WebKit draws the page out of process.

tauri-plugin-sbrs-automationthe plugin's repository
src
lib.rsinit, the settings, the bridge to the window
server.rsthe HTTP server on 127.0.0.1, one request at a time
webdriver.rsthe routes, the replies and the error statuses
webdriver
actions.rschecks W3C actions and cuts them into ticks
bridge.tsthe page script, in TypeScript: edit this one
bridge.jsgenerated from bridge.ts; what the plugin embeds
window.rswindow commands: change, then wait until the window has it
native.rsthe app's window, through Tauri
screenshot.rsmacOS: the WKWebView's snapshot, as PNG
trace.rsthe SBRS_AUTOMATION_TRACE_WINDOW output
permissionssbrs-automation:default lets the page script send its results back
testsbridge.test.mjs runs bridge.js in a fake page under Node.js

Security#

Warning

The endpoint can run any JavaScript in your app’s window; that is what WebDriver’s “execute script” is for. So it is guarded:

  • on in debug builds only, unless you set SBRS_AUTOMATION=1;
  • listens on 127.0.0.1 only;
  • every URL must start with the secret token;
  • requests from web pages (any request with an Origin header) are refused.

Keep it behind a Cargo feature that release builds do not enable.

License#

MIT.