Tauri App Testing
Testing patterns specific to Tauri v2 desktop applications -- the dual WebKit-first (local) + Chromium-CI strategy, the core crate pattern, backend bridge mocking, and the full 8-step escalation ladder.
WebKit First: Matching the Production Engine
Tauri's rendering engine is platform-dependent, not a single engine everywhere: WKWebView on macOS, WebKitGTK on Linux, and WebView2 (Chromium-based) on Windows. When writing Playwright E2E tests for a Tauri frontend, include WebKit to match the macOS/Linux production engine, and include Chromium too — it matches Windows exactly and runs more reliably in headless CI:
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
projects: [
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
});Warning
Testing only in Chromium gives false confidence for macOS and Linux users — those platforms render the production app in WebKit (WKWebView / WebKitGTK), so Chromium-only coverage can miss real WebKit-specific bugs.
Recommended dual strategy:
WebKit for local verification — matches the production engine on macOS and Linux. Use this for any test that exercises WebKit-specific behavior (CSS quirks, focus handling, keyboard shortcuts via the app's shortcut engine).
Chromium for CI — more reliable in headless CI environments, and for Windows users it isn't a tradeoff at all: Tauri's Windows webview (WebView2) is Chromium-based, so a Chromium CI run gives Windows real engine coverage for free.
The key rule: always verify locally with WebKit for macOS/Linux coverage, and keep both projects in your Playwright matrix. Using Chromium in CI for DOM-only tests is a pragmatic tradeoff for macOS/Linux, and genuine coverage for Windows — not a shortcut either way.
The Core Crate Pattern
Extract platform-independent business logic into a separate Rust crate that has no Tauri dependencies. This crate can be tested with standard cargo test without needing a Tauri application context:
src-tauri/
Cargo.toml # depends on core + tauri
src/
main.rs # Tauri setup, commands
commands.rs # #[tauri::command] handlers
core/
Cargo.toml # no Tauri dependency
src/
lib.rs
settings.rs # pure Rust logic
file_ops.rs # file operations
transforms.rs # data transforms # core/Cargo.toml
[package]
name = "myapp-core"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# No tauri dependency here// core/src/settings.rs
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Settings {
pub theme: String,
pub font_size: u32,
}
impl Settings {
pub fn with_theme(mut self, theme: &str) -> Self {
self.theme = theme.to_string();
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_with_theme() {
let settings = Settings {
theme: "light".to_string(),
font_size: 14,
};
let updated = settings.with_theme("dark");
assert_eq!(updated.theme, "dark");
assert_eq!(updated.font_size, 14);
}
}Tip
The core crate pattern lets you run cargo test in CI without building the full Tauri app. This is fast, reliable, and catches logic bugs early.
Backend Bridge Mock Adapter Pattern
In a Tauri app, the frontend communicates with the Rust backend through IPC commands. For frontend testing, mock this bridge:
// src/adapters/backend.ts -- the real adapter
import { invoke } from "@tauri-apps/api/core";
export interface BackendAdapter {
getSettings(): Promise<Settings>;
saveSettings(settings: Settings): Promise<void>;
readFile(path: string): Promise<string>;
}
export const tauriBackend: BackendAdapter = {
async getSettings() {
return invoke<Settings>("get_settings");
},
async saveSettings(settings) {
return invoke("save_settings", { settings });
},
async readFile(path) {
return invoke<string>("read_file", { path });
},
};// src/adapters/mock-backend.ts -- for testing
import type { BackendAdapter, Settings } from "./backend";
export function createMockBackend(
overrides: Partial<BackendAdapter> = {}
): BackendAdapter {
return {
async getSettings() {
return { theme: "dark", fontSize: 14 };
},
async saveSettings() {},
async readFile() {
return "mock file content";
},
...overrides,
};
}// In the app entry point
import { tauriBackend } from "./adapters/backend";
import { createMockBackend } from "./adapters/mock-backend";
// Runtime detection, not build-mode detection: import.meta.env.MODE === "test"
// only ever fires under Vitest. Playwright drives the built app in a real
// browser, so the mock must be selected by checking for the Tauri WebView's
// injected global -- otherwise E2E runs would hit the real IPC bridge.
const backend =
"__TAURI_INTERNALS__" in window ? tauriBackend : createMockBackend();The 8-Step Escalation Ladder
For Tauri apps, the escalation ladder extends beyond the standard testing levels:
| Step | Method | What It Catches |
|---|---|---|
| 1 | cargo test on core crate | Pure Rust logic bugs |
| 2 | Vitest unit tests | Frontend logic bugs |
| 3 | Vitest + jsdom component tests | Component behavior bugs |
| 4 | Playwright WebKit (dev server) | Frontend rendering bugs |
| 5 | Playwright WebKit (production build) | Build-specific frontend bugs |
| 6 | verify-ui + headless-browser | CSS/visual bugs in frontend |
| 7 | Tauri dev mode manual test | IPC integration bugs |
| 8 | Tauri production build manual test | Full app packaging bugs |
Note
Steps 1-6 are automatable and should be in CI. Steps 7-8 require the full Tauri application and are typically manual or require specialized CI with display servers.
Step-by-Step Guide
Steps 1-3: Fast, automatable, no browser needed
# Step 1: Rust core logic
cd core && cargo test
# Step 2: Frontend unit tests
pnpm vitest --project unit
# Step 3: Frontend component tests
pnpm vitest --project componentSteps 4-6: Need a browser, still automatable
# Step 4: E2E against dev server (WebKit only)
# playwright.config.ts's webServer starts `pnpm dev` and waits for it to be
# ready -- no manual backgrounding, so Playwright can't race an unready server.
npx playwright test --project=webkit
# Step 5: E2E against production build
pnpm build
# webServer's command is `pnpm preview` here (see CI Configuration below);
# Playwright starts it and waits for port 4173 before any test runs.
npx playwright test --project=webkit
# Step 6: Visual verification -- extract computed styles + screenshots, then
# compare the JSON `styles` output against the expected values (verify-ui is
# a plain Node script, not a CLI with its own --check flag)
node $HOME/.claude/skills/verify-ui/scripts/verify-styles.mjs \
"http://localhost:4173" ".app" "./verify-ui-out" "400,800,1200" "light,dark"Steps 7-8: Need full Tauri app
# Step 7: Tauri dev mode
pnpm tauri dev
# Manual testing in the actual Tauri window
# Step 8: Production build
pnpm tauri build
# Test the built .dmg / .msi / .AppImageCI Configuration for Tauri Projects
Point webServer at the already-built preview server instead of backgrounding a build+boot chain in the workflow step — this mirrors the Production Build Verification pattern from the sibling Playwright page:
// playwright.config.ts (excerpt)
export default defineConfig({
webServer: {
command: "pnpm preview",
port: 4173,
reuseExistingServer: !process.env.CI,
},
// ...projects from "WebKit First: Matching the Production Engine" above
});# .github/workflows/test.yml
jobs:
rust-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cd core && cargo test
frontend-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- run: pnpm install
- run: pnpm vitest run
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- run: pnpm install
- run: npx playwright install webkit --with-deps
- run: pnpm build
- run: npx playwright test --project=webkitWarning
Tauri E2E tests need the WebKit browser binary installed regardless of OS — Playwright always drives its own bundled WebKit build, never the system's Safari. npx playwright install webkit is required on every runner, macOS included. Only the --with-deps flag (which installs Linux system libraries WebKit needs) is Ubuntu-specific; on a macOS runner, npx playwright install webkit alone is enough.
The Scheduled Re-exam as Safety Net
Tauri keyboard-shortcut and native-platform tests fall into the platform-incapable category: they are only trustworthy on real WebKit/macOS. The CI configuration above covers the CI-safe layers (frontend unit tests and DOM-only Playwright specs), but the thin native-integration layer — @interactive and @macos-only tagged specs — requires a scheduled macOS job as the safety net.
The local heavy lane (exam) is convenient but bypassable; it is also machine-dependent (the same suite false-reds dozens of specs on a Linux/WSL2 host). A scheduled CI re-exam on a macOS runner provides the paper trail that makes these platform-bound specs part of a real regression gate.
See Scheduled Re-exam & Night Exam for the complete workflow: the cron + workflow_dispatch skeleton, deduped failure issue filing, and on-demand pre-merge dispatch.