Drive a Tauri app's webview from seleniumbase-rs tests on macOS, Windows and Linux (WebDriver inside the app)
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 (WKWebViewThe system web view of macOS, which Tauri apps use there.) has none. This plugin runs a small W3C WebDriver endpoint inside your app, so the same tests drive the app everywhere.
- 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
Every route with its fields, replies and errors.
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.
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.
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:
Register it
Register it:
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
capabilities
e2e
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:
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.
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):
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.
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.
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"
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"
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"
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.
| Command | Endpoint | Notes |
|---|---|---|
| Status | GET /status | No session needed. |
| New and delete session | POST /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. |
| Timeouts | GET, POST /timeouts | script, pageLoad, implicit (finding looks again until it is over). |
| Navigation | POST /url, GET /url, GET /title, POST /back, /forward, /refresh | Waits for the new page to load. |
| Page source | GET /source | Of the current frame. |
| Window | GET /window, /window/handles, POST /window, GET/POST /window/rect, POST /window/maximize, /minimize, /fullscreen | One 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. |
| Frames | POST /frame, POST /frame/parent | Same-origin frames only. |
| Find elements | POST /element, /elements, /element/{id}/element, /element/{id}/elements, GET /element/active | CSS, XPath, link text, partial link text, tag name. |
| Element state | GET /element/{id}/text, /name, /rect, /enabled, /selected, /displayed, /attribute/{name}, /property/{name}, /css/{name} | |
| Element interaction | POST /element/{id}/click, /clear, /value | DOM events. |
| Scripts | POST /execute/sync, /execute/async | Run in the current frame. |
| Actions | POST /actions, DELETE /actions | Mouse, keys and pauses. |
| User prompts | GET /alert/text, POST /alert/accept, /alert/dismiss, /alert/text | Simulated: answered when they open. |
| Screenshots | GET /screenshot, GET /element/{id}/screenshot | macOS only; PNG. |
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#
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.
How alert, confirm and prompt are replaced, recorded and answered.
Same-origin frames only, and what acts in the frame entered.
Tick by tick, as DOM events: the events the page sees, and why CSS :hover
and HTML drag and drop do not happen.
WKWebView’s own snapshot at the screen’s scale; an element’s part of it.
Set Window Rect first brings the window back to its normal state, as WebDriver asks: out of full screen, not maximized, not minimized.
Every error code the plugin answers, its HTTP status, and when.
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.
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
webdriver
Security#
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.1only; - every URL must start with the secret token;
- requests from web pages (any request with an
Originheader) are refused.
Keep it behind a Cargo feature that release builds do not enable.
License#
MIT.