.claude/skills/tauri-2/tauri-impl/tauri-impl-testing/SKILL.md
Use when writing tests for Tauri 2 commands, mocking IPC calls, or setting up E2E test suites. Prevents untestable command handlers and stale mock state from missing clearMocks() between tests. Covers Rust unit testing, frontend IPC mocking with mockIPC/mockWindows, WebDriver E2E testing, and integration testing. Keywords: tauri testing, mockIPC, mockWindows, clearMocks, cargo test, E2E testing, WebDriver, Vitest.
npx skillsauth add OpenAEC-Foundation/OpenAEC-Workspace-Composer tauri-impl-testingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
| Layer | Tool | What It Tests |
|-------|------|---------------|
| Rust Unit Tests | cargo test | Command logic, state management, error handling |
| Frontend Unit Tests | Vitest / Jest + @tauri-apps/api/mocks | UI components, IPC call handling |
| Integration Tests | Vitest / Jest + mockIPC | Frontend-backend contract |
| E2E Tests | WebDriver (Selenium/Playwright) | Full application behavior |
import { mockIPC, mockWindows, mockConvertFileSrc, clearMocks } from '@tauri-apps/api/mocks';
| Function | Purpose | Scope |
|----------|---------|-------|
| mockIPC(handler, options?) | Intercept invoke() calls | All commands |
| mockWindows(current, ...rest) | Mock window labels | Window API |
| mockConvertFileSrc(platform) | Mock file-to-URL conversion | Asset protocol |
| clearMocks() | Remove all mocks | All |
ALWAYS call clearMocks() in afterEach() -- failing to do so leaks mocks between tests, causing flaky test results.
NEVER test Tauri commands by running them through IPC in unit tests -- test the underlying Rust function directly. Commands are regular functions.
NEVER import @tauri-apps/api modules in test files without mocking first -- they throw errors outside a Tauri webview context.
ALWAYS use mockIPC before any invoke() call in frontend tests -- without it, invoke() attempts to use the actual IPC bridge which does not exist in a test environment.
NEVER forget to handle the Promise<UnlistenFn> return type when testing event listeners -- listen() returns a Promise, not a synchronous function.
Tauri commands decorated with #[tauri::command] are plain Rust functions. Test them directly without any Tauri runtime:
// src-tauri/src/commands.rs
#[tauri::command]
pub fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
#[tauri::command]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_greet() {
let result = greet("World".to_string());
assert_eq!(result, "Hello, World!");
}
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
assert_eq!(add(-1, 1), 0);
}
}
Run with cargo test from the src-tauri/ directory.
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("not found: {0}")]
NotFound(String),
#[error("invalid input: {0}")]
InvalidInput(String),
}
impl serde::Serialize for AppError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: serde::ser::Serializer {
serializer.serialize_str(self.to_string().as_ref())
}
}
#[tauri::command]
pub fn parse_number(input: String) -> Result<i64, AppError> {
input.parse::<i64>().map_err(|_| AppError::InvalidInput(input))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_valid() {
assert_eq!(parse_number("42".to_string()), Ok(42));
}
#[test]
fn test_parse_invalid() {
let result = parse_number("abc".to_string());
assert!(result.is_err());
}
}
Commands that use tauri::State cannot be tested directly with a State parameter. Extract the business logic into separate functions:
use std::sync::Mutex;
#[derive(Default)]
pub struct Counter {
pub value: u32,
}
// Business logic -- easily testable
pub fn increment_counter(counter: &mut Counter) -> u32 {
counter.value += 1;
counter.value
}
// Tauri command -- thin wrapper
#[tauri::command]
pub fn increment(state: tauri::State<'_, Mutex<Counter>>) -> u32 {
let mut counter = state.lock().unwrap();
increment_counter(&mut counter)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_increment() {
let mut counter = Counter::default();
assert_eq!(increment_counter(&mut counter), 1);
assert_eq!(increment_counter(&mut counter), 2);
assert_eq!(increment_counter(&mut counter), 3);
}
}
import { mockIPC, clearMocks } from '@tauri-apps/api/mocks';
import { invoke } from '@tauri-apps/api/core';
import { describe, it, expect, afterEach } from 'vitest';
afterEach(() => {
clearMocks();
});
describe('greet command', () => {
it('returns greeting message', async () => {
mockIPC((cmd, args) => {
if (cmd === 'greet') {
return `Hello, ${(args as Record<string, unknown>).name}!`;
}
throw new Error(`Unknown command: ${cmd}`);
});
const result = await invoke<string>('greet', { name: 'Test' });
expect(result).toBe('Hello, Test!');
});
it('handles errors', async () => {
mockIPC((cmd) => {
if (cmd === 'risky_operation') {
throw new Error('Operation failed');
}
});
await expect(invoke('risky_operation')).rejects.toThrow('Operation failed');
});
});
import { mockWindows, clearMocks } from '@tauri-apps/api/mocks';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { afterEach, it, expect } from 'vitest';
afterEach(() => {
clearMocks();
});
it('identifies current window', () => {
// First argument is the "current" window label
// Remaining arguments are other window labels
mockWindows('main', 'settings', 'about');
const win = getCurrentWindow();
expect(win.label).toBe('main');
});
import { mockConvertFileSrc, clearMocks } from '@tauri-apps/api/mocks';
import { convertFileSrc } from '@tauri-apps/api/core';
import { afterEach, it, expect } from 'vitest';
afterEach(() => {
clearMocks();
});
it('converts file paths to URLs', () => {
mockConvertFileSrc('linux');
const url = convertFileSrc('/path/to/image.png');
expect(url).toContain('image.png');
});
import { mockIPC, clearMocks } from '@tauri-apps/api/mocks';
import { listen, emit } from '@tauri-apps/api/event';
mockIPC(
(cmd, args) => {
// Handle commands
if (cmd === 'get_data') return { value: 42 };
},
{ shouldMockEvents: true }
);
// Now event APIs also work in tests
const unlisten = await listen('test-event', (event) => {
console.log(event.payload);
});
await emit('test-event', 'hello');
unlisten();
clearMocks();
import { render, screen, waitFor } from '@testing-library/react';
import { mockIPC, clearMocks } from '@tauri-apps/api/mocks';
import { afterEach, beforeEach, it, expect } from 'vitest';
import { MyComponent } from './MyComponent';
beforeEach(() => {
mockIPC((cmd, args) => {
if (cmd === 'get_user') {
return { name: 'Alice', age: 30 };
}
});
});
afterEach(() => {
clearMocks();
});
it('displays user data from backend', async () => {
render(<MyComponent />);
await waitFor(() => {
expect(screen.getByText('Alice')).toBeInTheDocument();
});
});
#[tauri::command]
pub async fn fetch_data(url: String) -> Result<String, String> {
reqwest::get(&url)
.await
.map_err(|e| e.to_string())?
.text()
.await
.map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_fetch_data() {
// Use a mock HTTP server or known endpoint
let result = fetch_data("https://httpbin.org/get".to_string()).await;
assert!(result.is_ok());
}
}
For async Rust tests, add tokio as a dev dependency:
[dev-dependencies]
tokio = { version = "1", features = ["rt", "macros"] }
Tauri apps can be tested with WebDriver (Selenium, Playwright) by enabling the webview's DevTools protocol.
Step 1: Enable DevTools in development:
WebviewWindowBuilder::new(app, "main", WebviewUrl::App("/".into()))
.devtools(true)
.build()?;
Step 2: Use Playwright or Selenium to connect to the webview:
// playwright.config.ts (conceptual -- Tauri-specific setup required)
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
// Connect to the Tauri app's webview via CDP
connectOptions: {
wsEndpoint: 'ws://localhost:9222',
},
},
});
Note: E2E testing with Tauri requires platform-specific WebDriver setup. The recommended approach is:
npm run tauri build -- --debugtauri-driver (a WebDriver server for Tauri apps)cargo install tauri-driver
# Start tauri-driver (listens on port 4444 by default)
tauri-driver
Then use any WebDriver client to control the app.
my-tauri-app/
├── src/
│ ├── components/
│ │ ├── Greeting.tsx
│ │ └── Greeting.test.tsx # Component + IPC tests
│ └── __tests__/
│ └── integration.test.ts # Cross-component tests
├── src-tauri/
│ └── src/
│ ├── commands.rs # Commands
│ └── commands_test.rs # OR tests in same file
├── e2e/
│ ├── app.spec.ts # E2E tests
│ └── setup.ts # WebDriver setup
└── vitest.config.ts
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test-setup.ts'],
},
});
// src/test-setup.ts
import { afterEach } from 'vitest';
import { clearMocks } from '@tauri-apps/api/mocks';
afterEach(() => {
clearMocks();
});
development
Use when integrating Vite with a backend framework, rendering Vite assets from server-side templates, or setting up dev/production HTML serving. Prevents incorrect manifest.json traversal and missing CSS chunk resolution in production. Covers build.manifest configuration, .vite/manifest.json structure, ManifestChunk properties, dev mode HTML setup, production rendering, CSS/JS chunk resolution, and modulepreload polyfill. Keywords: backend integration, manifest.json, ManifestChunk, Django, Laravel, Rails, modulepreload.
development
Use when encountering dev server startup failures, HMR issues, proxy errors, CORS blocks, or module not found errors during development. Prevents misconfiguring server.hmr behind reverse proxies and forgetting appType: 'custom' in middleware mode. Covers HMR full-reload debugging, proxy configuration, CORS setup, HTTPS certificates, server.fs.strict violations, port conflicts, WebSocket failures, file watcher issues, and middleware mode. Keywords: dev server, HMR, proxy, CORS, HTTPS, WebSocket, port conflict, server.fs.strict, middleware mode, file watcher.
development
Use when encountering pre-bundling errors, dependency resolution failures, stale cache issues, or slow development server startup. Prevents excluding CJS dependencies from pre-bundling (which breaks runtime module resolution) and misconfiguring optimizeDeps. Covers CJS/ESM conversion failures, missing dependency auto-discovery, optimizeDeps configuration, monorepo linked dependencies, cache invalidation, browser cache staleness, and large dependency tree performance. Keywords: pre-bundling, optimizeDeps, CJS, ESM, cache, dependency resolution, monorepo, node_modules/.vite.
development
Use when encountering Vite build failures, chunk size warnings, or version-specific build errors. Prevents the common mistake of using deprecated rollupOptions in v8 or misconfiguring build targets and minifiers. Covers Rolldown/Rollup bundling failures, CSS minification errors, sourcemap problems, library mode build failures, BundleError handling, and asset processing errors. Keywords: build error, Rolldown, chunk size, sourcemap, library mode, minify, BundleError, rollupOptions, build.target.