tauri-plugin-sync-state 0.1.0

Bidirectional, type-keyed state sync between a Tauri backend and a JS/React frontend.
Documentation
//! Unit coverage for the pure-logic surface of the plugin: the `#[derive(SyncState)]`
//! macro and the `StateRegistry` (type-keyed storage, JSON bridge, error paths).
//! None of this needs a running Tauri app, so it lives as a plain integration test.

use serde::{Deserialize, Serialize};
use serde_json::json;

use tauri_plugin_sync_state::{Error, StateRegistry, SyncState};

#[derive(SyncState, Serialize, Deserialize, Clone, Default, PartialEq, Debug)]
struct Counter {
    value: i32,
}

#[derive(SyncState, Serialize, Deserialize, Clone, Default, PartialEq, Debug)]
struct Settings {
    dark_mode: bool,
}

#[derive(SyncState, Serialize, Deserialize, Clone, Default)]
#[sync_state(name = "my_custom_key")]
struct Renamed {
    x: i32,
}

#[test]
fn derive_defaults_name_to_type_ident() {
    assert_eq!(Counter::NAME, "Counter");
    assert_eq!(Settings::NAME, "Settings");
}

#[test]
fn derive_honours_custom_name_attribute() {
    assert_eq!(Renamed::NAME, "my_custom_key");
}

#[test]
fn insert_then_read_round_trips() {
    let registry = StateRegistry::new();
    registry.insert(Counter { value: 7 }).unwrap();

    let read = registry.read::<Counter>().unwrap();
    assert_eq!(read, Counter { value: 7 });
}

#[test]
fn mutate_updates_and_returns_the_new_value() {
    let registry = StateRegistry::new();
    registry.insert(Counter { value: 1 }).unwrap();

    let returned = registry.mutate::<Counter>(|c| c.value += 41).unwrap();
    assert_eq!(returned, Counter { value: 42 });

    // The mutation is persisted in the registry, not just reflected in the return.
    assert_eq!(registry.read::<Counter>().unwrap(), Counter { value: 42 });
}

#[test]
fn distinct_state_types_are_keyed_independently() {
    let registry = StateRegistry::new();
    registry.insert(Counter { value: 3 }).unwrap();
    registry.insert(Settings { dark_mode: true }).unwrap();

    assert_eq!(registry.read::<Counter>().unwrap().value, 3);
    assert!(registry.read::<Settings>().unwrap().dark_mode);
}

#[test]
fn duplicate_registration_is_rejected() {
    let registry = StateRegistry::new();
    registry.insert(Counter { value: 0 }).unwrap();

    let err = registry.insert(Counter { value: 9 }).unwrap_err();
    assert!(matches!(err, Error::DuplicateRegistration(name) if name == "Counter"));

    // The original value must be untouched by the failed second insert.
    assert_eq!(registry.read::<Counter>().unwrap().value, 0);
}

#[test]
fn reading_an_unregistered_slice_errors() {
    let registry = StateRegistry::new();
    let err = registry.read::<Counter>().unwrap_err();
    assert!(matches!(err, Error::NotFound(name) if name == "Counter"));
}

#[test]
fn mutating_an_unregistered_slice_errors() {
    let registry = StateRegistry::new();
    let err = registry.mutate::<Counter>(|c| c.value = 1).unwrap_err();
    assert!(matches!(err, Error::NotFound(name) if name == "Counter"));
}

#[test]
fn json_bridge_round_trips_by_name() {
    let registry = StateRegistry::new();
    registry.insert(Counter { value: 5 }).unwrap();

    assert_eq!(registry.get_json("Counter").unwrap(), json!({ "value": 5 }));

    registry
        .set_json("Counter", json!({ "value": 100 }))
        .unwrap();
    assert_eq!(registry.read::<Counter>().unwrap().value, 100);
    assert_eq!(
        registry.get_json("Counter").unwrap(),
        json!({ "value": 100 })
    );
}

#[test]
fn set_json_with_a_mismatched_shape_errors_and_preserves_state() {
    let registry = StateRegistry::new();
    registry.insert(Counter { value: 5 }).unwrap();

    let err = registry
        .set_json("Counter", json!({ "value": "not-an-int" }))
        .unwrap_err();
    assert!(matches!(err, Error::Deserialize(_)));

    // A rejected deserialization must leave the existing value intact.
    assert_eq!(registry.read::<Counter>().unwrap().value, 5);
}

#[test]
fn get_and_set_json_on_an_unknown_name_error() {
    let registry = StateRegistry::new();
    assert!(matches!(
        registry.get_json("Nope").unwrap_err(),
        Error::NotFound(_)
    ));
    assert!(matches!(
        registry.set_json("Nope", json!({})).unwrap_err(),
        Error::NotFound(_)
    ));
}

#[test]
fn contains_reflects_registration() {
    let registry = StateRegistry::new();
    assert!(!registry.contains("Counter"));

    registry.insert(Counter { value: 0 }).unwrap();
    assert!(registry.contains("Counter"));
    assert!(!registry.contains("Settings"));
}