<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mathieu</title>
    <description>The latest articles on DEV Community by Mathieu (@mpiton).</description>
    <link>https://dev.to/mpiton</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3862049%2F99ddf1bd-c90e-45d0-9ed3-eda43a629685.png</url>
      <title>DEV Community: Mathieu</title>
      <link>https://dev.to/mpiton</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mpiton"/>
    <language>en</language>
    <item>
      <title>I built a CLI to test Tauri apps because nothing else worked</title>
      <dc:creator>Mathieu</dc:creator>
      <pubDate>Sun, 05 Apr 2026 09:29:22 +0000</pubDate>
      <link>https://dev.to/mpiton/i-built-a-cli-to-test-tauri-apps-because-nothing-else-worked-3915</link>
      <guid>https://dev.to/mpiton/i-built-a-cli-to-test-tauri-apps-because-nothing-else-worked-3915</guid>
      <description>&lt;p&gt;I spent a weekend trying to set up end-to-end testing for a Tauri v2 app. Two hours into configuring WebdriverIO, I still couldn't get it to connect to the WebView. The official docs show a minimal example that doesn't cover IPC testing. Playwright flat-out doesn't work because Tauri uses WebKitGTK, not Chromium.&lt;/p&gt;

&lt;p&gt;I gave up and wrote my own tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual problem
&lt;/h2&gt;

&lt;p&gt;If you search "Tauri e2e testing" on GitHub, you'll find the same question asked over and over. The &lt;a href="https://v2.tauri.app/develop/tests/" rel="noopener noreferrer"&gt;official docs&lt;/a&gt; split testing into two worlds: Rust unit tests for the backend, and WebDriver-based tests for the frontend. But nobody tells you how to verify that your Tauri IPC commands actually work from the user's perspective. You end up mocking &lt;code&gt;window.__TAURI__&lt;/code&gt; in your frontend tests and hoping production behaves the same way.&lt;/p&gt;

&lt;p&gt;It doesn't always behave the same way.&lt;/p&gt;

&lt;p&gt;What I wanted was simple: connect to a running Tauri app, inspect the UI, click buttons, fill forms, and check that things happened. Like Playwright, but for Tauri. No WebDriver binary. No Selenium. No 200-line config file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What tauri-pilot does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/mpiton/tauri-pilot" rel="noopener noreferrer"&gt;tauri-pilot&lt;/a&gt; is a Rust CLI that talks to your Tauri app over a Unix socket. You add a plugin to your app (2 lines, debug builds only), install the CLI, and start testing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;tauri-pilot snapshot &lt;span class="nt"&gt;-i&lt;/span&gt;
- heading &lt;span class="s2"&gt;"PR Dashboard"&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;e1]
- textbox &lt;span class="s2"&gt;"Search PRs"&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;e2] &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
- button &lt;span class="s2"&gt;"Refresh"&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;e3]
- list &lt;span class="s2"&gt;"PR List"&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;e4]
  - listitem &lt;span class="s2"&gt;"fix: resolve memory leak #142"&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;e5]
  - listitem &lt;span class="s2"&gt;"feat: add workspace support #138"&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;e6]
- button &lt;span class="s2"&gt;"Load More"&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;e7]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;snapshot&lt;/code&gt; command walks the accessibility tree and gives every interactive element a short ref like &lt;code&gt;@e3&lt;/code&gt;. You use those refs to interact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;tauri-pilot click @e3
ok

&lt;span class="nv"&gt;$ &lt;/span&gt;tauri-pilot fill @e2 &lt;span class="s2"&gt;"workspace"&lt;/span&gt;
ok

&lt;span class="nv"&gt;$ &lt;/span&gt;tauri-pilot assert text @e1 &lt;span class="s2"&gt;"PR Dashboard"&lt;/span&gt;
ok

&lt;span class="nv"&gt;$ &lt;/span&gt;tauri-pilot assert visible @e7
ok
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit code 0 means pass. Exit code 1 means fail with a message. That's it. No test framework to learn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup takes 2 minutes
&lt;/h2&gt;

&lt;p&gt;Add the plugin to your Tauri app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src-tauri/src/main.rs&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nd"&gt;#[cfg(debug_assertions)]&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="nf"&gt;.plugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;tauri_plugin_pilot&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="nf"&gt;.run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;generate_context!&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"error running app"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;tauri-pilot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run your app in dev mode. That's it. &lt;code&gt;tauri-pilot ping&lt;/code&gt; should respond.&lt;/p&gt;

&lt;p&gt;The plugin only compiles in debug builds. It won't end up in your release binary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use WebdriverIO?
&lt;/h2&gt;

&lt;p&gt;I tried. Here's what the experience looks like:&lt;/p&gt;

&lt;p&gt;With WebdriverIO, you need to install Node.js dependencies, configure &lt;code&gt;wdio.conf.ts&lt;/code&gt;, point it at your built binary (not the dev server), make sure the WebDriver binary matches your WebKit version, write tests in JavaScript even though your app is in Rust, and deal with flaky selectors that break when your UI changes.&lt;/p&gt;

&lt;p&gt;With tauri-pilot, you run &lt;code&gt;cargo install tauri-pilot&lt;/code&gt;, add 2 lines to your app, and start writing shell commands. The accessibility tree refs (&lt;code&gt;@e1&lt;/code&gt;, &lt;code&gt;@e2&lt;/code&gt;) are stable across renders as long as the element exists. You can use CSS selectors too, or pixel coordinates for edge cases.&lt;/p&gt;

&lt;p&gt;The diff command is something WebdriverIO can't do at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;tauri-pilot diff &lt;span class="nt"&gt;-i&lt;/span&gt;
+ button &lt;span class="s2"&gt;"New PR"&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;e8]
~ list &lt;span class="s2"&gt;"PR List"&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;e4]: children 2 → 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It compares the current UI state to the previous snapshot and shows only what changed. When you're debugging a UI issue, that's worth more than any assertion framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's built for AI agents
&lt;/h2&gt;

&lt;p&gt;I initially built tauri-pilot so Claude Code could interact with my Tauri apps. The output format is designed for LLM consumption: compact accessibility tree, short refs, plain text responses.&lt;/p&gt;

&lt;p&gt;An AI agent's workflow looks like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;snapshot -i&lt;/code&gt; to understand the current page&lt;/li&gt;
&lt;li&gt;Pick a ref from the output and interact with it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;assert&lt;/code&gt; to verify the result&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;diff -i&lt;/code&gt; to see what changed without re-reading the entire tree (saves tokens)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;--json&lt;/code&gt; flag on every command gives structured output for programmatic use.&lt;/p&gt;

&lt;p&gt;But you don't need an AI agent to benefit from this. I use tauri-pilot directly in bash scripts and interactively while developing. It's faster than clicking through the UI manually when I'm testing a form flow for the fifth time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in v0.2.0
&lt;/h2&gt;

&lt;p&gt;The latest release (just shipped) adds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Record/replay&lt;/strong&gt; — &lt;code&gt;tauri-pilot record start&lt;/code&gt;, interact with your app, &lt;code&gt;record stop --output test.json&lt;/code&gt;, then &lt;code&gt;replay test.json&lt;/code&gt; to rerun the whole session. Export to a shell script with &lt;code&gt;replay --export sh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-window support&lt;/strong&gt; — &lt;code&gt;windows&lt;/code&gt; lists all windows, &lt;code&gt;--window&lt;/code&gt; flag targets a specific one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Form dump&lt;/strong&gt; — &lt;code&gt;forms&lt;/code&gt; grabs all form fields at once instead of calling &lt;code&gt;value&lt;/code&gt; on each input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;localStorage/sessionStorage access&lt;/strong&gt; — &lt;code&gt;storage get "token"&lt;/code&gt;, &lt;code&gt;storage set "key" "value"&lt;/code&gt;, &lt;code&gt;storage list&lt;/code&gt;, &lt;code&gt;storage clear&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drag &amp;amp; drop&lt;/strong&gt; — &lt;code&gt;drag @e5 @e6&lt;/code&gt; for sortable lists, &lt;code&gt;drop @e3 --file ./image.png&lt;/code&gt; for file upload zones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DOM watch&lt;/strong&gt; — &lt;code&gt;watch&lt;/code&gt; blocks until a DOM mutation happens, then prints a summary. Good for waiting on async updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full changelog is on &lt;a href="https://github.com/mpiton/tauri-pilot/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works under the hood
&lt;/h2&gt;

&lt;p&gt;The plugin starts a Unix socket server when your app launches. The CLI connects to that socket and sends JSON-RPC messages. The plugin injects a JS bridge (&lt;code&gt;window.__PILOT__&lt;/code&gt;) into the WebView that handles DOM inspection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F992gs9jdb0u5cyx1mlee.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F992gs9jdb0u5cyx1mlee.jpeg" alt="tauri-pilot architecture diagram" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The tricky part was getting return values from &lt;code&gt;webview.eval()&lt;/code&gt;. In Tauri v2, &lt;code&gt;eval()&lt;/code&gt; is fire-and-forget. There's no way to get a result back directly. So every JS evaluation wraps the script in a try/catch, calls back into Rust via IPC (&lt;code&gt;invoke('plugin:pilot|__callback', {id, result})&lt;/code&gt;), and the Rust side waits on a oneshot channel with a 10-second timeout.&lt;/p&gt;

&lt;p&gt;It works reliably. I've been running it daily for weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;Linux only for now. Tauri uses WebKitGTK on Linux, WKWebView on macOS, and WebView2 on Windows. The socket and JS bridge approach should work on all platforms, but I haven't tested it. macOS and Windows support is planned.&lt;/p&gt;

&lt;p&gt;The screenshot command uses html-to-image bundled into the JS bridge, which means it captures the WebView content but not native window decorations or system dialogs.&lt;/p&gt;

&lt;p&gt;And it's pre-1.0. The API might change. I'm using semver, so minor bumps can include breaking changes until 1.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;tauri-pilot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/mpiton/tauri-pilot" rel="noopener noreferrer"&gt;github.com/mpiton/tauri-pilot&lt;/a&gt;&lt;br&gt;
Docs: &lt;a href="https://mpiton.github.io/tauri-pilot" rel="noopener noreferrer"&gt;mpiton.github.io/tauri-pilot&lt;/a&gt;&lt;br&gt;
crates.io: &lt;a href="https://crates.io/crates/tauri-plugin-pilot" rel="noopener noreferrer"&gt;crates.io/crates/tauri-plugin-pilot&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're testing a Tauri v2 app on Linux, give it a shot. Issues and PRs are open.&lt;/p&gt;

</description>
      <category>tauri</category>
      <category>rust</category>
      <category>testing</category>
      <category>cli</category>
    </item>
  </channel>
</rss>
