Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TauriTest

A UI integration testing library for Tauri, that exposes Tauri commands to JSdom via N-API bridge, allowing full JS-driven integration testing against application UI and Rust source code.

Getting Started

In your Tauri app crate, add tauri-test:

[lib]
crate-type = ["cdylib", "rlib", "staticlib"]

[build-dependencies]
napi-build = "2"

[dependencies]
tauri = "2"
tauri-test = "0.2.1"
napi = { version = "2", default-features = false, features = ["napi8", "async", "serde-json"] }
napi-derive = "2"
serde_json = "1"

Add serde with the derive feature only if a command takes/returns a custom type, or you use the state option below (both need #[derive(Serialize/Deserialize)]):

serde = { version = "1", features = ["derive"] }

Use napi_build::setup() in build.rs:

fn main() {
    napi_build::setup();
    tauri_build::build();
}

Tauri commands will automatically get invoke(...):

#[tauri_test::setup(init = init_test_state, state = AppState)]
pub struct App;

// `state = AppState` needs `Serialize` so `getAppState()` can return it as JSON.
#[derive(serde::Serialize)]
struct AppState {
    label: String,
}

// optional: initialize the Rust state your commands read via `tauri::State`
fn init_test_state() -> (TodoDb, AppState) {
    (TodoDb::new(), AppState { label: "integration-test".into() })
}

#[tauri::command]
fn greet(name: String) -> String {
    format!("Hello, {name}! You've been greeted from Rust!")
}

setup options

Both keys are optional and may be given in any order.

Key Purpose
init = path::to::fn Returns the state values to register. Return a tuple to register several.
state = SomeType Exposes a getAppState() export returning that registered value as JSON. SomeType must implement serde::Serialize.

Supported command parameters

Parameter Provided by the harness as
Any serde::Deserialize type The matching key from the invoke args object
Option<T> The key if present, otherwise None
tauri::State<'_, T> The value registered by your init function
tauri::AppHandle / tauri::WebviewWindow Whatever was passed to tauri_test::runtime::register_app_handle / register_webview_window

Commands taking an AppHandle or WebviewWindow dispatch normally, but fail with a descriptive error unless a handle has been registered. A handle can only come from a harness that builds a real Apptauri::test::mock_app produces an App<MockRuntime>, whose handle type differs from the AppHandle<Wry> your commands expect, so there is no automatic fallback.

Return types

T, Result<T, E>, and single-type-argument aliases whose name ends in Result (e.g. type AppResult<T> = Result<T, String>) are all unwrapped, so invoke resolves with the bare value and rejects on Err. Because a proc macro cannot resolve type aliases, the alias check is name-based: an unrelated single-generic type named QueryResult<T> would also be treated as a Result.

Running the tests

Build the src-tauri library to generate the addon loader:

{
  "devDependencies": {
    "vitest": "^3.0.0",
    "@testing-library/react": "^16.0.0",
    "@testing-library/jest-dom": "^6.0.0",
    "jsdom": "^26.0.0"
  },
  "scripts": {
    "pretest": "cd src-tauri && cargo build --lib",
    "test": "vitest run"
  }
}

In tests/setup.ts, load the compiled addon and register a Vitest mock for @tauri-apps/api/core:

import { createRequire } from "node:module";
import { vi } from "vitest";

const require = createRequire(import.meta.url);

export const tauriTest = require("../src-tauri/target") as {
  invoke: (cmd: string, args?: Record<string, unknown>) => Promise<unknown>;
  getAppState: () => { label: string };
};

vi.mock("@tauri-apps/api/core", () => ({
  invoke: tauriTest.invoke
}));

Inspecting app state

When #[tauri_test::setup] is given state = SomeType, the addon also exports getAppState(), which serializes the registered value to JSON. Useful for asserting on state your UI does not surface:

expect(tauriTest.getAppState()).toEqual({ label: "integration-test" });

Write a real integration test:

import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import App from "../src/App";

it("greets through real Rust", async () => {
  render(<App />);

  await userEvent.type(screen.getByPlaceholderText("Enter a name..."), "World");
  await userEvent.click(screen.getByRole("button", { name: /greet/i }));

  await waitFor(() => {
    expect(
      screen.getByText(/Hello, World! You've been greeted from Rust!/i),
    ).toBeInTheDocument();
  });
});

About

UI integration testing for Tauri

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages