Easy Tauri integration tests with vitest

kobieps1 pts0 comments

Easy Tauri Integration Tests with Vitest<br>About<br>Open-SourcePricingBlogDocs<br>Engineering

May 14, 2026<br>|4min. read

Easy Tauri Integration Tests with Vitest<br>Having a hard time writing integration tests for Tauri? It's very easy with this little trick, even on macOS!

By Simon Binder

← Blog<br>Recently, we introduced our PowerSync SDK for Tauri.<br>The SDK is a Tauri plugin, meaning that it consists of two parts:

A Rust crate used to access a local SQLite database, sync changes via PowerSync, and notify JavaScript apps about changes.

A small JavaScript library acting as a type-safe wrapper around the raw Tauri IPC commands supported by our Rust crate.

As part of the development work, we also looked at ways to test our new SDK.<br>Tauri basically offers three modes of testing:

A MockRuntime for Rust, allowing us to write unit tests for our Rust crate by mocking out JavaScript.

A fake Tauri environment for JavaScript, allowing us to write unit tests for our JavaScript package by mocking out Rust.

A WebDriver integration, allowing us to test everything together.

Especially when writing Tauri plugins, options 1 and 2 provide very little value.<br>How our JavaScript and Rust sources interact is the most interesting thing we want to test, as our SDK wouldn't work if anything about that was wrong.<br>Two tests each mocking the other half provide no guarantees.

So the only option left available to us was to spin up a demo app and use WebDriver support built into Tauri for integration tests.<br>However, that option has its own big issues:

It's designed to test Tauri apps, while we're interested in testing plugin functionality.

It doesn't work on macOS, which is what most of us use.

In the end, we found a neat way to very reliably test everything in our Tauri plugin nonetheless: Instead of driving an app through WebDriver,<br>what if we launched a Tauri app that simply... tested itself?<br>After all, this is exactly how we test most of our JavaScript nowadays: Instead of mocking web or React APIs to run tests in Node, we let vitest<br>spawn a browser and use the real thing. That loads a web page running our tests, reporting results back to a local vitest server which will print them and set<br>an appropriate exit code for CI.

Using Tauri as a vitest browser

Starting from version 4, vitest has pretty decent support for custom browsers. So our plan to test our SDK was fairly straightforward:

Write a tiny Tauri app that allows loading any URL and loads the PowerSync Tauri plugin.

Tell vitest to launch that app with a custom URL instead of spawning a browser.

You can see the whole thing in action here.

The main.rs for our test app is very simple: It receives a URL as a CLI argument before opening a window with that URL.<br>Using dev_url skips some IPC checks and simplifies the setup:

use std::env;<br>use url::Url;

fn main() {<br>// Use default options, but open window with URL from args.<br>let mut context = tauri::generate_context!();<br>if let Some(url) = env::args().skip(1).next() {<br>let config = context.config_mut();<br>config.build.dev_url = Some(Url::parse(&url).expect("Could not parse URL"));

tauri::Builder::default()<br>.plugin(tauri_plugin_powersync::init())<br>.run(context)<br>.expect("error while running tauri application");

For permissions in capabilities/default.json, we used these:

"$schema": "../gen/schemas/desktop-schema.json",<br>"identifier": "default",<br>"description": "enables the default permissions",<br>"windows": ["*"],<br>"remote": {<br>"urls": ["http://127.0.0.1:*"]<br>},<br>"permissions": [<br>"core:default",<br>"powersync:default",<br>"core:webview:allow-create-webview-window",<br>"core:window:allow-set-title"<br>This would not be a good idea for real Tauri apps, but permission checks would just stand in the way for integration tests.

To launch this app in vitest, we wrote a custom BrowserProvider in vitest.config.ts:

const serverFactory = preview().serverFactory;<br>// Relative path to the integration test runner app built with cargo.<br>const testRunnerExecutable = path.resolve('../../target/debug/test-runner');

class TauriBrowserProvider implements BrowserProvider {<br>#tauriApp?: ChildProcess;<br>#isClosing = false;

// ... some boring methods omitted

async openPage(_sessionId: string, url: string, _options: { parallel: boolean; }) {<br>if (this.#tauriApp != null) {<br>throw new Error('TODO: Calling openPage multiple times is not supported');

// Ensure the target app spawning webviews is up-to-date.<br>const buildResult = spawnSync('cargo', ['build', '-p', 'test-runner'], { stdio: 'inherit' });<br>if (buildResult.status !== 0) {<br>throw new Error(`cargo build failed with exit code ${buildResult.status}`);<br>const app = spawn(testRunnerExecutable, [url]);<br>this.#tauriApp = app;

app.on('exit', (code) => {<br>if (!this.#isClosing) {<br>console.log('Test runner exited with code', code);<br>process.exit(1);<br>});

await new Promisevoid>((resolve, reject) => {<br>app.once('spawn', () => resolve());<br>app.once('error', reject);<br>});

async close() {<br>this.#isClosing = true;<br>this.#tauriApp?.kill();<br>And that's...

tauri test tests vitest integration javascript

Related Articles