In plugins/fs/guest-js/index.ts, the size function only sent `path`
although the Rust command accepts `options.baseDir` and the JSDoc example
passed `{ baseDir: BaseDirectory.AppData }` (a TS error, silently dropped
at runtime, so the relative path was rejected).
Adds an optional `options?: SizeOptions` parameter (new exported type).
api-iife.js regenerated. The e2e spec now measures a file and a directory
relative to AppData instead of working around the missing option.
Plugins end-to-end tests
WebdriverIO suite that exercises the JavaScript API of the plugins in this
repository against a real Tauri app — the examples/api
validation app — rather than a mocked backend, on desktop (Linux, macOS, Windows)
and mobile (Android, iOS). Each plugin has its own spec file, shared by every
platform, and adding coverage for a new plugin API is normally just dropping in
one more spec.
It mirrors the @tauri-apps/api e2e suite
in the core repository; the @tauri-apps/api modules themselves are covered there.
How it works
- The example app is built with
withGlobalTauri: true, so the@tauri-apps/apisurface is reachable onwindow.__TAURI__inside the webview, and every plugin'sapi-iife.jsregisters its API next to it (window.__TAURI__.fs,window.__TAURI__.clipboardManager, …, the package name without the@tauri-apps/plugin-prefix, camel-cased). - On desktop, WebdriverIO drives the app through
@crabnebula/tauri-driver, which bridges the WebDriver protocol to each platform's webview:- macOS — the CrabNebula Webdriver, which needs
tauri-plugin-automation(registered inexamples/apibehind its off-by-defaultautomationCargo feature, which the suite's build enables) and a locally-running@crabnebula/test-runner-backend, authenticated withCN_API_KEY. - Linux —
webkit2gtk-driver(WebKitWebDriveronPATH). - Windows —
msedgedriver.exeonPATH. It hands the app the--remote-debugging-portit attaches to throughWEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS, which WebView2 ignores in an elevated process (wry#1782), so the suite has to run unelevated.
- macOS — the CrabNebula Webdriver, which needs
- On mobile, WebdriverIO drives the app through Appium (started by
@wdio/appium-service; the drivers are plain devDependencies of this package, which Appium picks up on its own):- Android — the UiAutomator2 driver. The suite switches to the app's
WEBVIEW_*context, which chromedriver reaches through the WebView's debugging socket. Debug builds turn that on (setWebContentsDebuggingEnabled), so the suite builds a debug APK. A chromedriver matching the device's WebView is downloaded on demand (seeE2E_CHROMEDRIVER). - iOS — the XCUITest driver on a simulator, attaching to the WKWebView through the
WebKit remote inspector. Debug builds mark the webview
isInspectable, so the suite builds an unsigned debug simulator app. The inspector identifies an app by theapplication-identifierentitlement that Xcode embeds when it code signs a simulator build; an unsigned one has none and is listed asprocess-<executable name>instead of its bundle identifier, so the config has the driver match that name too (appium:additionalWebviewBundleIds). The driver also starts with a script timeout of 0, which the config raises to the 30s the other drivers default to, or everyexecuteAsyncwould time out at once.
- Android — the UiAutomator2 driver. The suite switches to the app's
- Specs never
evalin the page. They pass a function to thetauri()helper, which serializes it and runs it via the driver's own (CSP-exempt) script injection, handing itwindow.__TAURI__as the first argument and returning its JSON result. - Each spec file gets its own session — a fresh
tauri-driver(and therefore a fresh app instance) on desktop, a fresh Appium session (which relaunches the app) on mobile — so each plugin's suite runs in isolation. - A small fixture server is started for the whole run. It serves
the updater manifest the desktop e2e build points the updater at (see
tauri.e2e.conf.json) and the upload/download fixtures. It listens on the host's loopback; the iOS simulator shares that network stack, and on Android the mobile config runsadb reverseso the same127.0.0.1URL works on the device. Thehttpspecs use the echo server the example app itself spawns on port 3003, since that is the onlyhttp://origin in the example's http scope. The fixture server also has a WebSocket echo endpoint (ws://127.0.0.1:3004/ws) for thewebsocketspecs.
What is (not) covered
The example only registers each plugin on the platforms it supports, so the suites split three ways: desktop-only plugins are skipped on mobile, mobile-only plugins are skipped on desktop, and the rest run everywhere with the odd test gated.
| Plugin | Coverage | On mobile |
|---|---|---|
autostart |
enable/disable/isEnabled, restoring the host's original state. | Skipped — desktop-only plugin. |
cli |
getMatches shape for an argument-less launch. |
Skipped — desktop-only plugin. |
clipboard-manager |
text, HTML and image round-trips, clear, error paths. |
Text only; HTML and images are unsupported there. |
deep-link |
getCurrent on a plain launch, onOpenUrl delivery, runtime register/isRegistered/unregister on Linux and Windows. |
Only that runtime registration is unsupported. |
dialog |
Only that the API is registered — every dialog blocks on native UI the driver cannot operate. | Same. |
fs |
read/write/stat/copy/rename/remove/truncate, FileHandle, line iteration, watchers, scope. |
All but the watchers (fs:allow-watch is desktop-only here). |
global-shortcut |
register/unregister/isRegistered/unregisterAll and error paths. Shortcuts cannot be triggered. | Skipped — desktop-only plugin. |
http |
fetch methods, headers, JSON/bytes/multipart bodies, the cookie jar, abort, scope, failures. |
Same. |
log |
attachLogger/attachConsole for webview and Rust records, level filtering. |
Same. |
notification |
Permission model and the window.Notification override. Sending is fire-and-forget and display is not observable. |
Action types, pending/active lists after cancel/remove, channels, listeners — see below. |
opener |
Scope enforcement only — a successful open launches an external app the suite cannot close. | Same. |
os |
Every function but version (which has nothing to compare against), checked against what the app was built for and the host. |
Same, minus hostname. |
positioner |
Screen and tray positions (with a tray rect handed in through handleIconState), moveWindowConstrained, the missing-tray error (WM dependent). |
Skipped — desktop-only plugin. |
process |
Only that the API is registered — exit/relaunch terminate the app under test. |
Same. |
shell |
execute, spawn with stdout/stderr/close events, stdin, kill, scope enforcement. |
Scope only on iOS, which cannot spawn a process at all. |
sql |
SQLite load/execute/select/close, bound values, column types, app-registered migrations, error paths. |
Same. |
store |
CRUD, persistence, auto-save, defaults/reset, reload, getStore, LazyStore, change events. |
Same. |
stronghold |
Store records (persisted across reloads), wrong-password and unknown-client errors, vault secrets, BIP39/SLIP10 derivation and Ed25519 signing. | Same. |
updater |
check against the fixture manifest (update / 204 / older release). Installing is never exercised. |
Skipped — desktop-only plugin. |
upload |
download and upload with progress, methods, headers and error paths. |
Same (through adb reverse on Android). |
websocket |
Text/binary echo, ping/pong, handshake headers, listener removal, server and client close, connection and argument errors, against the fixture server. | Same (through adb reverse on Android). |
window-state |
filename, save/restore, and that a re-created window gets its saved size (WM dependent). |
Skipped — desktop-only plugin. |
| mobile-only plugins | barcode-scanner, biometric, geolocation, haptics and nfc need hardware or native UI the driver cannot operate, so only their global API surface is asserted. |
Only there. |
The notification permission specs are desktop-only: a mobile app starts out ungranted and
requestPermission puts up a system dialog the session would then block on. The rest of the
notification API (action types, channels, the pending/active lists, listeners) only exists on
mobile, and is covered there without the permission.
The plugins.spec.ts spec additionally asserts that every
plugin the platform registers injects its global API with its documented members, and that
the other platform's plugins are absent. That is what catches a plugin whose
global_api_script_path is missing from its build.rs, or a crate that is not target-gated
in the example's Cargo.toml.
Prerequisites
# from the repo root
pnpm install
pnpm build # examples/api resolves the plugins' JS from their dist-js output
Platform driver dependencies:
| Platform | Requirement |
|---|---|
| macOS | CN_API_KEY env var (CrabNebula Cloud). The automation plugin and test-runner-backend are wired up already. |
| Linux | webkit2gtk-driver package (provides WebKitWebDriver). |
| Windows | msedgedriver.exe matching your Edge version, on PATH. Run the suite unelevated. |
| Android | The usual Tauri Android setup (ANDROID_HOME, NDK_HOME, a JDK), which also provides the adb the suite shells out to, plus a running emulator or a connected device with USB debugging. Network access the first time, for the chromedriver download. |
| iOS | macOS with Xcode and an iOS simulator runtime. The first session compiles WebDriverAgent (a few minutes). |
Running
# desktop, from the repo root
pnpm test:api-e2e
# or from this package
pnpm e2e
# iterate without rebuilding the app every run
E2E_SKIP_BUILD=1 pnpm e2e
# run a single plugin's spec
pnpm exec wdio run ./wdio.conf.ts --spec test/specs/fs.spec.ts
# mobile (from the repo root; or `pnpm e2e:android` / `pnpm e2e:ios` from this package)
pnpm test:api-e2e:android
pnpm test:api-e2e:ios
The first desktop run builds the app with tauri.e2e.conf.json as a
config override, which enables the example's automation feature and points the updater at
the fixture server; afterwards use E2E_SKIP_BUILD=1 to reuse the existing binary. A binary
supplied through E2E_SKIP_BUILD or E2E_APP_PATH must have been built with that override:
the updater specs rely on its endpoint, and the CrabNebula Webdriver (always on macOS)
relies on the automation feature.
The mobile configs (wdio.android.conf.ts, wdio.ios.conf.ts,
sharing wdio.mobile.ts) run tauri android build --debug --apk /
tauri ios build --debug --target aarch64-sim --no-sign, compiling only the Rust target the
device runs (the Android one is read from the connected device through adb). The Android
Studio and Xcode projects are committed under examples/api/src-tauri/gen, so they are only
initialized if that directory is missing. The app must be a debug build — release builds
have webview debugging off, and Appium cannot see the page. E2E_SKIP_BUILD and
E2E_APP_PATH (an .apk / simulator .app) work as on desktop. No config override is
passed there: both things it turns on belong to desktop-only plugins.
Environment variables
| Variable | Purpose |
|---|---|
CN_API_KEY |
CrabNebula Cloud key. Required on macOS (and whenever E2E_CN_WEBDRIVER=1). |
E2E_SKIP_BUILD |
Skip the tauri build step and reuse the existing binary. |
E2E_APP_PATH |
Absolute path to a prebuilt app/binary to test (also implies skip-build). |
E2E_SKIP |
Comma-separated plugin names to skip, e.g. E2E_SKIP=clipboard-manager,global-shortcut. |
E2E_SKIP_WM |
Skip window-manager-dependent tests (the window-state size restore). |
E2E_SPEC_RETRIES |
Retry count for flaky spec files (default 0). |
E2E_CN_WEBDRIVER |
Use the CrabNebula Webdriver on Linux/Windows too (instead of the native driver). |
E2E_NATIVE_DRIVER |
Path passed to tauri-driver --native-driver (e.g. a specific chromedriver). |
CARGO_TARGET_DIR |
Override the target dir the app binary is looked up in. |
Mobile only:
| Variable | Purpose |
|---|---|
E2E_ANDROID_TARGET |
Rust target for the APK (aarch64, armv7, i686, x86_64); default: the connected device's ABI. |
E2E_ANDROID_DEVICE |
adb serial of the device/emulator to use (appium:udid); default: the first connected one. |
E2E_ANDROID_AVD |
Name of an AVD for Appium to boot (appium:avd) instead of using an already-running emulator. |
E2E_CHROMEDRIVER |
chromedriver binary matching the device's WebView, instead of letting Appium download one. |
E2E_IOS_TARGET |
Rust target for the simulator app (aarch64-sim or x86_64); default: the host architecture. |
E2E_IOS_DEVICE |
Simulator UDID or name (as in xcrun simctl list); default: a booted iPhone, else the newest one. |
E2E_PLATFORM |
Set by the mobile configs for the spec workers (android/ios) — see platform in the helpers. |
Appium's own log is written to logs/wdio-appium.log in this package.
Adding tests for a new plugin API
-
Add a spec. Create
test/specs/<plugin>.spec.tsand usedescribePlugin('<plugin>', …)with thetauri()helper. It is picked up automatically by thetest/specs/**/*.spec.tsglob. Minimal example:import { expect } from '@wdio/globals' import { tauri, describePlugin } from '../helpers/index.js' describePlugin('os', () => { it('reports the platform', async () => { expect(await tauri((api) => api.os.platform())).toBe('linux') }) })If the plugin is new to the example, register it in
examples/api/src-tauri/src/lib.rs, add it to the example'spackage.json/Cargo.toml, add itsworkspace:*package to this package'sdevDependenciesand its namespace to the matching interface intest/helpers/index.ts—CommonPluginApi,DesktopPluginApiorMobilePluginApi— plus the member list inplugins.spec.ts. -
Grant permissions if needed. If the API calls a command that the example does not allow yet, add the permission to
examples/api/src-tauri/capabilities/base.json(ordesktop.json/mobile.jsonfor platform-specific plugins). Each plugin documents its permissions underplugins/<plugin>/permissions/autogenerated/reference.md. -
Need a server? Add a route to the fixture server rather than hitting the network; its URL is exported as
FIXTURE_SERVER_URL. -
Handle environment-sensitive cases. Use
itWm(instead ofit) for assertions that depend on a real window manager, andeventually()to poll for state that is applied asynchronously. Branch onplatformfrom the helpers (neverprocess.platform, which is the host running the emulator/simulator on mobile) for platform-specific behavior. Files go underscratchDir('<plugin>'), relative toBaseDirectory.AppData, which the example's fs scope allows. -
Gate what mobile does not have. The same specs run on Android and iOS. Wrap tests of desktop-only behavior — a command the mobile build does not expose, a mobile implementation that answers "Unsupported on this platform", or a permission only the desktop capability grants — in
itDesktop, useitOn('android', …)/itOn('ios', …)for platform-specific APIs, and pass{ desktopOnly: true }(or{ mobileOnly: true }) todescribePluginfor plugins the example does not register on the other side. Skipped tests show up as pending rather than silently disappearing.
Rules for tauri() page functions
The function you pass to tauri() runs inside the webview, serialized as a string:
- It cannot close over anything from the spec module — pass every value it needs through
the trailing
tauri(fn, ...args)arguments. - It may only reference
api(thewindow.__TAURI__object), those args, and browser globals (window,document,setTimeout,Promise, …). - Its return value must be JSON-serializable — return plain objects/primitives, not class
instances (call methods and return their results instead), and remember that
undefinedvalues are dropped. - Restore any app state you mutate (clipboard, registered shortcuts, window size, …); tests within a spec file share the same app instance.
- For in-page waiting, wrap logic in a
Promisewith an explicitsetTimeoutrejection so a failure surfaces as a message rather than an opaque driver timeout.
Use tauriError(fn, ...args) to assert that a call rejects; it returns the rejection message.