shelly-test 0.4.0

Testing helpers and macros for Shelly LiveView apps.
Documentation
//! Test helpers for Shelly LiveView apps.
//!
//! This crate provides:
//! - small constructors for protocol events
//! - assertion helpers for common `ServerMessage` shapes
//! - macros to keep tests concise

pub use serde_json;
pub use shelly;

use shelly::{ClientMessage, DynamicSlotPatch, LiveSession, ServerMessage, StreamPosition};

/// Build a `ClientMessage::Event` with explicit fields.
pub fn client_event(
    name: impl Into<String>,
    target: Option<String>,
    value: serde_json::Value,
    metadata: serde_json::Map<String, serde_json::Value>,
) -> ClientMessage {
    ClientMessage::Event {
        event: name.into(),
        target,
        value,
        metadata,
    }
}

/// Dispatch one client message into a live session.
pub fn dispatch(session: &mut LiveSession, message: ClientMessage) -> Vec<ServerMessage> {
    session.handle_client_message(message)
}

/// Expect exactly one `patch` message.
pub fn expect_single_patch(messages: &[ServerMessage]) -> (&str, &str, u64) {
    match messages {
        [ServerMessage::Patch {
            target,
            html,
            revision,
        }] => (target.as_str(), html.as_str(), *revision),
        _ => panic!("expected exactly one patch message, got: {messages:?}"),
    }
}

/// Expect exactly one `diff` message.
pub fn expect_single_diff(messages: &[ServerMessage]) -> (&str, u64, &[DynamicSlotPatch]) {
    match messages {
        [ServerMessage::Diff {
            target,
            revision,
            slots,
        }] => (target.as_str(), *revision, slots.as_slice()),
        _ => panic!("expected exactly one diff message, got: {messages:?}"),
    }
}

/// Expect exactly one `stream_insert` message.
pub fn expect_single_stream_insert(
    messages: &[ServerMessage],
) -> (&str, &str, &str, &StreamPosition) {
    match messages {
        [ServerMessage::StreamInsert {
            target,
            id,
            html,
            at,
        }] => (target.as_str(), id.as_str(), html.as_str(), at),
        _ => panic!("expected exactly one stream_insert message, got: {messages:?}"),
    }
}

/// Expect exactly one `stream_delete` message.
pub fn expect_single_stream_delete(messages: &[ServerMessage]) -> (&str, &str) {
    match messages {
        [ServerMessage::StreamDelete { target, id }] => (target.as_str(), id.as_str()),
        _ => panic!("expected exactly one stream_delete message, got: {messages:?}"),
    }
}

/// Expect exactly one `error` message.
pub fn expect_single_error(messages: &[ServerMessage]) -> (&str, Option<&str>) {
    match messages {
        [ServerMessage::Error { message, code }] => (message.as_str(), code.as_deref()),
        _ => panic!("expected exactly one error message, got: {messages:?}"),
    }
}

/// Build a `ClientMessage::Event`.
#[macro_export]
macro_rules! event {
    ($name:expr $(,)?) => {
        $crate::client_event(
            $name,
            None,
            $crate::serde_json::Value::Null,
            $crate::serde_json::Map::new(),
        )
    };
    ($name:expr, value = $value:expr $(,)?) => {
        $crate::client_event($name, None, $value, $crate::serde_json::Map::new())
    };
    ($name:expr, target = $target:expr $(,)?) => {
        $crate::client_event(
            $name,
            Some(($target).to_string()),
            $crate::serde_json::Value::Null,
            $crate::serde_json::Map::new(),
        )
    };
    ($name:expr, target = $target:expr, value = $value:expr $(,)?) => {
        $crate::client_event(
            $name,
            Some(($target).to_string()),
            $value,
            $crate::serde_json::Map::new(),
        )
    };
    ($name:expr, value = $value:expr, target = $target:expr $(,)?) => {
        $crate::client_event(
            $name,
            Some(($target).to_string()),
            $value,
            $crate::serde_json::Map::new(),
        )
    };
    ($name:expr, target = $target:expr, value = $value:expr, metadata = $metadata:expr $(,)?) => {
        $crate::client_event($name, Some(($target).to_string()), $value, $metadata)
    };
    ($name:expr, value = $value:expr, target = $target:expr, metadata = $metadata:expr $(,)?) => {
        $crate::client_event($name, Some(($target).to_string()), $value, $metadata)
    };
}

/// Mount a fresh live session from a `Default` live view.
#[macro_export]
macro_rules! mount_session {
    ($view_ty:ty $(,)?) => {{
        let mut session = $crate::shelly::LiveSession::new(Box::<$view_ty>::default(), "root");
        session
            .mount()
            .expect("mount_session! should mount live view");
        session
    }};
    ($view_ty:ty, target = $target:expr $(,)?) => {{
        let mut session = $crate::shelly::LiveSession::new(Box::<$view_ty>::default(), $target);
        session
            .mount()
            .expect("mount_session! should mount live view");
        session
    }};
}

/// Dispatch one message into the session.
#[macro_export]
macro_rules! dispatch {
    ($session:expr, $message:expr $(,)?) => {
        $crate::dispatch(&mut $session, $message)
    };
}

/// Assert one patch message with exact target/revision.
#[macro_export]
macro_rules! assert_patch {
    ($messages:expr, target = $target:expr, revision = $revision:expr $(,)?) => {{
        let (actual_target, _actual_html, actual_revision) =
            $crate::expect_single_patch(&($messages));
        assert_eq!(actual_target, $target, "unexpected patch target");
        assert_eq!(actual_revision, $revision, "unexpected patch revision");
    }};
    ($messages:expr, target = $target:expr, revision = $revision:expr, html = $html:expr $(,)?) => {{
        let (actual_target, actual_html, actual_revision) =
            $crate::expect_single_patch(&($messages));
        assert_eq!(actual_target, $target, "unexpected patch target");
        assert_eq!(actual_revision, $revision, "unexpected patch revision");
        assert_eq!(actual_html, $html, "unexpected patch html");
    }};
    ($messages:expr, target = $target:expr, revision = $revision:expr, html_contains = $needle:expr $(,)?) => {{
        let (actual_target, actual_html, actual_revision) =
            $crate::expect_single_patch(&($messages));
        assert_eq!(actual_target, $target, "unexpected patch target");
        assert_eq!(actual_revision, $revision, "unexpected patch revision");
        assert!(
            actual_html.contains($needle),
            "expected patch html to contain `{}`, actual html: {}",
            $needle,
            actual_html
        );
    }};
}

/// Assert one diff message.
#[macro_export]
macro_rules! assert_diff {
    ($messages:expr, target = $target:expr, revision = $revision:expr, slots_len = $slots_len:expr $(,)?) => {{
        let (actual_target, actual_revision, actual_slots) =
            $crate::expect_single_diff(&($messages));
        assert_eq!(actual_target, $target, "unexpected diff target");
        assert_eq!(actual_revision, $revision, "unexpected diff revision");
        assert_eq!(actual_slots.len(), $slots_len, "unexpected diff slot count");
    }};
}

/// Assert one stream-insert message.
#[macro_export]
macro_rules! assert_stream_insert {
    ($messages:expr, target = $target:expr, id = $id:expr $(,)?) => {{
        let (actual_target, actual_id, _actual_html, _actual_at) =
            $crate::expect_single_stream_insert(&($messages));
        assert_eq!(actual_target, $target, "unexpected stream target");
        assert_eq!(actual_id, $id, "unexpected stream id");
    }};
}

/// Assert one stream-delete message.
#[macro_export]
macro_rules! assert_stream_delete {
    ($messages:expr, target = $target:expr, id = $id:expr $(,)?) => {{
        let (actual_target, actual_id) = $crate::expect_single_stream_delete(&($messages));
        assert_eq!(actual_target, $target, "unexpected stream target");
        assert_eq!(actual_id, $id, "unexpected stream id");
    }};
}

/// Assert one error code.
#[macro_export]
macro_rules! assert_error_code {
    ($messages:expr, $code:expr $(,)?) => {{
        let (_actual_message, actual_code) = $crate::expect_single_error(&($messages));
        assert_eq!(actual_code, Some($code), "unexpected error code");
    }};
    ($messages:expr, none $(,)?) => {{
        let (_actual_message, actual_code) = $crate::expect_single_error(&($messages));
        assert_eq!(actual_code, None, "expected no error code");
    }};
}