629 lines
20 KiB
Rust
629 lines
20 KiB
Rust
//! Integration tests for p2p-chat
|
|
//!
|
|
//! Covers:
|
|
//! - Protocol message serialization roundtrips
|
|
//! - Config defaults, TOML parsing, and color parsing
|
|
//! - TUI App command dispatch & key handling
|
|
//! - ChatState history management
|
|
|
|
// ============================================================================
|
|
// Protocol tests
|
|
// ============================================================================
|
|
mod protocol_tests {
|
|
use p2p_chat::protocol::*;
|
|
|
|
#[test]
|
|
fn chat_message_roundtrip() {
|
|
let msg = GossipMessage::Chat(ChatMessage {
|
|
sender_name: "alice".into(),
|
|
timestamp: 1234567890,
|
|
text: "Hello, world!".into(),
|
|
});
|
|
let bytes = postcard::to_allocvec(&msg).unwrap();
|
|
let decoded: GossipMessage = postcard::from_bytes(&bytes).unwrap();
|
|
match decoded {
|
|
GossipMessage::Chat(m) => {
|
|
assert_eq!(m.sender_name, "alice");
|
|
assert_eq!(m.timestamp, 1234567890);
|
|
assert_eq!(m.text, "Hello, world!");
|
|
}
|
|
_ => panic!("Expected Chat variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn disconnect_message_roundtrip() {
|
|
let msg = GossipMessage::Disconnect {
|
|
sender_name: "bob".into(),
|
|
};
|
|
let bytes = postcard::to_allocvec(&msg).unwrap();
|
|
let decoded: GossipMessage = postcard::from_bytes(&bytes).unwrap();
|
|
match decoded {
|
|
GossipMessage::Disconnect { sender_name } => {
|
|
assert_eq!(sender_name, "bob");
|
|
}
|
|
_ => panic!("Expected Disconnect variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn name_change_roundtrip() {
|
|
let msg = GossipMessage::NameChange(NameChange {
|
|
old_name: "anon".into(),
|
|
new_name: "alice".into(),
|
|
});
|
|
let bytes = postcard::to_allocvec(&msg).unwrap();
|
|
let decoded: GossipMessage = postcard::from_bytes(&bytes).unwrap();
|
|
match decoded {
|
|
GossipMessage::NameChange(nc) => {
|
|
assert_eq!(nc.old_name, "anon");
|
|
assert_eq!(nc.new_name, "alice");
|
|
}
|
|
_ => panic!("Expected NameChange variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn peer_announce_roundtrip() {
|
|
let msg = GossipMessage::PeerAnnounce(PeerAnnounce {
|
|
sender_name: "charlie".into(),
|
|
});
|
|
let bytes = postcard::to_allocvec(&msg).unwrap();
|
|
let decoded: GossipMessage = postcard::from_bytes(&bytes).unwrap();
|
|
match decoded {
|
|
GossipMessage::PeerAnnounce(pa) => {
|
|
assert_eq!(pa.sender_name, "charlie");
|
|
}
|
|
_ => panic!("Expected PeerAnnounce variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn capabilities_default() {
|
|
let caps = CapabilitiesMessage::default();
|
|
assert!(caps.chat);
|
|
assert!(caps.files);
|
|
assert!(!caps.audio);
|
|
assert!(!caps.camera);
|
|
assert!(!caps.screen);
|
|
assert_eq!(caps.sender_name, "");
|
|
}
|
|
|
|
#[test]
|
|
fn file_offer_broadcast_roundtrip() {
|
|
let fid = new_file_id();
|
|
let msg = GossipMessage::FileOfferBroadcast(FileOfferBroadcast {
|
|
sender_name: "dave".into(),
|
|
file_id: fid,
|
|
file_name: "test.txt".into(),
|
|
file_size: 42,
|
|
timeout: 60,
|
|
});
|
|
let bytes = postcard::to_allocvec(&msg).unwrap();
|
|
let decoded: GossipMessage = postcard::from_bytes(&bytes).unwrap();
|
|
match decoded {
|
|
GossipMessage::FileOfferBroadcast(f) => {
|
|
assert_eq!(f.sender_name, "dave");
|
|
assert_eq!(f.file_id, fid);
|
|
assert_eq!(f.file_name, "test.txt");
|
|
assert_eq!(f.file_size, 42);
|
|
}
|
|
_ => panic!("Expected FileOfferBroadcast variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn encode_framed_produces_valid_length_prefix() {
|
|
let msg = ChatMessage {
|
|
sender_name: "test".into(),
|
|
timestamp: 0,
|
|
text: "hi".into(),
|
|
};
|
|
let framed = encode_framed(&msg).unwrap();
|
|
assert!(framed.len() > 4);
|
|
let len = u32::from_be_bytes([framed[0], framed[1], framed[2], framed[3]]) as usize;
|
|
assert_eq!(len, framed.len() - 4);
|
|
// Verify the payload can be deserialized
|
|
let decoded: ChatMessage = postcard::from_bytes(&framed[4..]).unwrap();
|
|
assert_eq!(decoded.sender_name, "test");
|
|
}
|
|
|
|
#[test]
|
|
fn file_id_is_unique() {
|
|
let id1 = new_file_id();
|
|
let id2 = new_file_id();
|
|
assert_ne!(id1, id2);
|
|
}
|
|
|
|
#[test]
|
|
fn media_kind_serialization() {
|
|
for kind in [MediaKind::Voice, MediaKind::Camera, MediaKind::Screen] {
|
|
let bytes = postcard::to_allocvec(&kind).unwrap();
|
|
let decoded: MediaKind = postcard::from_bytes(&bytes).unwrap();
|
|
assert_eq!(decoded, kind);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn media_stream_message_roundtrip() {
|
|
let msgs = vec![
|
|
MediaStreamMessage::AudioStart {
|
|
sample_rate: 48000,
|
|
channels: 1,
|
|
frame_size_ms: 20,
|
|
},
|
|
MediaStreamMessage::AudioData {
|
|
sequence: 42,
|
|
opus_data: vec![0xDE, 0xAD],
|
|
},
|
|
MediaStreamMessage::AudioStop,
|
|
MediaStreamMessage::VideoStart {
|
|
kind: MediaKind::Screen,
|
|
width: 1920,
|
|
height: 1080,
|
|
fps: 30,
|
|
},
|
|
MediaStreamMessage::VideoFrame {
|
|
sequence: 1,
|
|
timestamp_ms: 1000,
|
|
data: vec![0x01, 0x02, 0x03],
|
|
},
|
|
MediaStreamMessage::VideoStop {
|
|
kind: MediaKind::Camera,
|
|
},
|
|
];
|
|
for msg in msgs {
|
|
let bytes = postcard::to_allocvec(&msg).unwrap();
|
|
let _decoded: MediaStreamMessage = postcard::from_bytes(&bytes).unwrap();
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Config tests
|
|
// ============================================================================
|
|
mod config_tests {
|
|
use p2p_chat::config::*;
|
|
use ratatui::style::Color;
|
|
|
|
#[test]
|
|
fn default_config_values() {
|
|
let config = AppConfig::default();
|
|
// assert_eq!(config.media.screen_resolution, "1280x720");
|
|
// assert!(config.media.mic_name.is_none());
|
|
assert!(config.network.topic.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn config_toml_roundtrip() {
|
|
let config = AppConfig::default();
|
|
let toml_str = toml::to_string_pretty(&config).unwrap();
|
|
let parsed: AppConfig = toml::from_str(&toml_str).unwrap();
|
|
// assert_eq!(
|
|
// parsed.media.screen_resolution,
|
|
// config.media.screen_resolution
|
|
// );
|
|
// assert_eq!(parsed.media.mic_name, config.media.mic_name);
|
|
assert_eq!(parsed.network.topic, config.network.topic);
|
|
}
|
|
|
|
#[test]
|
|
fn config_partial_toml_uses_defaults() {
|
|
let toml_str = r#"
|
|
[media]
|
|
screen_resolution = "1920x1080"
|
|
"#;
|
|
let config: AppConfig = toml::from_str(toml_str).unwrap();
|
|
// assert_eq!(config.media.screen_resolution, "1920x1080");
|
|
// network should use default
|
|
assert!(config.network.topic.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_color_named_colors() {
|
|
assert_eq!(parse_color("red"), Color::Red);
|
|
assert_eq!(parse_color("green"), Color::Green);
|
|
assert_eq!(parse_color("blue"), Color::Blue);
|
|
assert_eq!(parse_color("cyan"), Color::Cyan);
|
|
assert_eq!(parse_color("magenta"), Color::Magenta);
|
|
assert_eq!(parse_color("yellow"), Color::Yellow);
|
|
assert_eq!(parse_color("white"), Color::White);
|
|
assert_eq!(parse_color("black"), Color::Black);
|
|
assert_eq!(parse_color("gray"), Color::Gray);
|
|
assert_eq!(parse_color("dark_gray"), Color::DarkGray);
|
|
assert_eq!(parse_color("darkgray"), Color::DarkGray);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_color_case_insensitive() {
|
|
assert_eq!(parse_color("RED"), Color::Red);
|
|
assert_eq!(parse_color("Green"), Color::Green);
|
|
assert_eq!(parse_color("BLUE"), Color::Blue);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_color_hex_6digit() {
|
|
assert_eq!(parse_color("#ff0000"), Color::Rgb(255, 0, 0));
|
|
assert_eq!(parse_color("#00ff00"), Color::Rgb(0, 255, 0));
|
|
assert_eq!(parse_color("#0000ff"), Color::Rgb(0, 0, 255));
|
|
assert_eq!(parse_color("#ffffff"), Color::Rgb(255, 255, 255));
|
|
assert_eq!(parse_color("#000000"), Color::Rgb(0, 0, 0));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_color_hex_3digit() {
|
|
assert_eq!(parse_color("#f00"), Color::Rgb(255, 0, 0));
|
|
assert_eq!(parse_color("#0f0"), Color::Rgb(0, 255, 0));
|
|
assert_eq!(parse_color("#00f"), Color::Rgb(0, 0, 255));
|
|
assert_eq!(parse_color("#fff"), Color::Rgb(255, 255, 255));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_color_unknown_falls_back_to_white() {
|
|
assert_eq!(parse_color("nonexistent"), Color::White);
|
|
assert_eq!(parse_color(""), Color::White);
|
|
}
|
|
|
|
#[test]
|
|
fn theme_from_ui_config() {
|
|
let ui = UiConfig::default();
|
|
let theme: Theme = ui.into();
|
|
assert_eq!(theme.chat_border, Color::Cyan);
|
|
assert_eq!(theme.text, Color::White);
|
|
assert_eq!(theme.self_name, Color::Green);
|
|
assert_eq!(theme.system_msg, Color::Yellow);
|
|
assert_eq!(theme.time, Color::DarkGray);
|
|
}
|
|
|
|
#[test]
|
|
fn ui_config_defaults() {
|
|
let ui = UiConfig::default();
|
|
assert_eq!(ui.text, "white");
|
|
assert_eq!(ui.self_name, "green");
|
|
assert_eq!(ui.system_msg, "yellow");
|
|
assert_eq!(ui.time, "dark_gray");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// TUI key handling tests
|
|
// ============================================================================
|
|
mod tui_tests {
|
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
|
use p2p_chat::config::{Theme, UiConfig};
|
|
use p2p_chat::tui::{App, InputMode, TuiCommand};
|
|
|
|
fn make_app() -> App {
|
|
let theme: Theme = UiConfig::default().into();
|
|
App::new(theme)
|
|
}
|
|
|
|
fn key(code: KeyCode) -> KeyEvent {
|
|
KeyEvent::new(code, KeyModifiers::NONE)
|
|
}
|
|
|
|
fn ctrl_key(code: KeyCode) -> KeyEvent {
|
|
KeyEvent::new(code, KeyModifiers::CONTROL)
|
|
}
|
|
|
|
#[test]
|
|
fn initial_state() {
|
|
let app = make_app();
|
|
assert_eq!(app.input_mode, InputMode::Editing);
|
|
assert_eq!(app.input, "");
|
|
assert_eq!(app.cursor_position, 0);
|
|
assert_eq!(app.scroll_offset, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn esc_switches_to_normal_mode() {
|
|
let mut app = make_app();
|
|
assert_eq!(app.input_mode, InputMode::Editing);
|
|
let cmd = app.handle_key(key(KeyCode::Esc));
|
|
assert!(matches!(cmd, TuiCommand::None));
|
|
assert_eq!(app.input_mode, InputMode::Normal);
|
|
}
|
|
|
|
#[test]
|
|
fn i_switches_to_editing_mode() {
|
|
let mut app = make_app();
|
|
app.input_mode = InputMode::Normal;
|
|
let cmd = app.handle_key(key(KeyCode::Char('i')));
|
|
assert!(matches!(cmd, TuiCommand::None));
|
|
assert_eq!(app.input_mode, InputMode::Editing);
|
|
}
|
|
|
|
#[test]
|
|
fn enter_in_normal_switches_to_editing() {
|
|
let mut app = make_app();
|
|
app.input_mode = InputMode::Normal;
|
|
let cmd = app.handle_key(key(KeyCode::Enter));
|
|
assert!(matches!(cmd, TuiCommand::None));
|
|
assert_eq!(app.input_mode, InputMode::Editing);
|
|
}
|
|
|
|
#[test]
|
|
fn slash_in_normal_prefills_slash() {
|
|
let mut app = make_app();
|
|
app.input_mode = InputMode::Normal;
|
|
let cmd = app.handle_key(key(KeyCode::Char('/')));
|
|
assert!(matches!(cmd, TuiCommand::None));
|
|
assert_eq!(app.input_mode, InputMode::Editing);
|
|
assert_eq!(app.input, "/");
|
|
assert_eq!(app.cursor_position, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn q_in_normal_quits() {
|
|
let mut app = make_app();
|
|
app.input_mode = InputMode::Normal;
|
|
let cmd = app.handle_key(key(KeyCode::Char('q')));
|
|
assert!(matches!(cmd, TuiCommand::Quit));
|
|
}
|
|
|
|
#[test]
|
|
fn typing_characters_in_editing() {
|
|
let mut app = make_app();
|
|
app.handle_key(key(KeyCode::Char('h')));
|
|
app.handle_key(key(KeyCode::Char('i')));
|
|
assert_eq!(app.input, "hi");
|
|
assert_eq!(app.cursor_position, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn backspace_deletes_character() {
|
|
let mut app = make_app();
|
|
app.handle_key(key(KeyCode::Char('a')));
|
|
app.handle_key(key(KeyCode::Char('b')));
|
|
app.handle_key(key(KeyCode::Char('c')));
|
|
assert_eq!(app.input, "abc");
|
|
app.handle_key(key(KeyCode::Backspace));
|
|
assert_eq!(app.input, "ab");
|
|
assert_eq!(app.cursor_position, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn enter_sends_message() {
|
|
let mut app = make_app();
|
|
app.handle_key(key(KeyCode::Char('h')));
|
|
app.handle_key(key(KeyCode::Char('i')));
|
|
let cmd = app.handle_key(key(KeyCode::Enter));
|
|
assert!(matches!(cmd, TuiCommand::SendMessage(ref s) if s == "hi"));
|
|
assert_eq!(app.input, "");
|
|
assert_eq!(app.cursor_position, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn enter_on_empty_input_does_nothing() {
|
|
let mut app = make_app();
|
|
let cmd = app.handle_key(key(KeyCode::Enter));
|
|
assert!(matches!(cmd, TuiCommand::None));
|
|
}
|
|
|
|
#[test]
|
|
fn quit_command() {
|
|
let mut app = make_app();
|
|
for c in "/quit".chars() {
|
|
app.handle_key(key(KeyCode::Char(c)));
|
|
}
|
|
let cmd = app.handle_key(key(KeyCode::Enter));
|
|
assert!(matches!(cmd, TuiCommand::Quit));
|
|
}
|
|
|
|
#[test]
|
|
fn help_command_is_system_message() {
|
|
let mut app = make_app();
|
|
for c in "/help".chars() {
|
|
app.handle_key(key(KeyCode::Char(c)));
|
|
}
|
|
let cmd = app.handle_key(key(KeyCode::Enter));
|
|
assert!(matches!(cmd, TuiCommand::SystemMessage(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn nick_command_without_name_is_system_message() {
|
|
let mut app = make_app();
|
|
for c in "/nick".chars() {
|
|
app.handle_key(key(KeyCode::Char(c)));
|
|
}
|
|
let cmd = app.handle_key(key(KeyCode::Enter));
|
|
assert!(matches!(cmd, TuiCommand::SystemMessage(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn nick_command_with_name() {
|
|
let mut app = make_app();
|
|
for c in "/nick alice".chars() {
|
|
app.handle_key(key(KeyCode::Char(c)));
|
|
}
|
|
let cmd = app.handle_key(key(KeyCode::Enter));
|
|
assert!(matches!(cmd, TuiCommand::ChangeNick(ref s) if s == "alice"));
|
|
}
|
|
|
|
#[test]
|
|
fn connect_command_without_id_is_system_message() {
|
|
let mut app = make_app();
|
|
for c in "/connect".chars() {
|
|
app.handle_key(key(KeyCode::Char(c)));
|
|
}
|
|
let cmd = app.handle_key(key(KeyCode::Enter));
|
|
assert!(matches!(cmd, TuiCommand::SystemMessage(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn connect_command_with_id() {
|
|
let mut app = make_app();
|
|
for c in "/connect abc123".chars() {
|
|
app.handle_key(key(KeyCode::Char(c)));
|
|
}
|
|
let cmd = app.handle_key(key(KeyCode::Enter));
|
|
assert!(matches!(cmd, TuiCommand::Connect(ref s) if s == "abc123"));
|
|
}
|
|
|
|
#[test]
|
|
fn voice_command() {
|
|
let mut app = make_app();
|
|
for c in "/voice".chars() {
|
|
app.handle_key(key(KeyCode::Char(c)));
|
|
}
|
|
let cmd = app.handle_key(key(KeyCode::Enter));
|
|
assert!(matches!(cmd, TuiCommand::ToggleVoice));
|
|
}
|
|
|
|
#[test]
|
|
fn camera_command() {
|
|
let mut app = make_app();
|
|
for c in "/camera".chars() {
|
|
app.handle_key(key(KeyCode::Char(c)));
|
|
}
|
|
let cmd = app.handle_key(key(KeyCode::Enter));
|
|
assert!(matches!(cmd, TuiCommand::ToggleCamera));
|
|
}
|
|
|
|
#[test]
|
|
fn screen_command() {
|
|
let mut app = make_app();
|
|
for c in "/screen".chars() {
|
|
app.handle_key(key(KeyCode::Char(c)));
|
|
}
|
|
let cmd = app.handle_key(key(KeyCode::Enter));
|
|
assert!(matches!(cmd, TuiCommand::ToggleScreen));
|
|
}
|
|
|
|
#[test]
|
|
fn leave_command() {
|
|
let mut app = make_app();
|
|
for c in "/leave".chars() {
|
|
app.handle_key(key(KeyCode::Char(c)));
|
|
}
|
|
let cmd = app.handle_key(key(KeyCode::Enter));
|
|
assert!(matches!(cmd, TuiCommand::Leave));
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_command_is_system_message() {
|
|
let mut app = make_app();
|
|
for c in "/foobar".chars() {
|
|
app.handle_key(key(KeyCode::Char(c)));
|
|
}
|
|
let cmd = app.handle_key(key(KeyCode::Enter));
|
|
assert!(matches!(cmd, TuiCommand::SystemMessage(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn file_command_with_path() {
|
|
let mut app = make_app();
|
|
for c in "/file /tmp/test.txt".chars() {
|
|
app.handle_key(key(KeyCode::Char(c)));
|
|
}
|
|
let cmd = app.handle_key(key(KeyCode::Enter));
|
|
match cmd {
|
|
TuiCommand::SendFile(path) => {
|
|
assert_eq!(path.to_str().unwrap(), "/tmp/test.txt");
|
|
}
|
|
_ => panic!("Expected SendFile, got {:?}", cmd),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn scroll_up_and_down_in_normal_mode() {
|
|
let mut app = make_app();
|
|
app.input_mode = InputMode::Normal;
|
|
app.handle_key(key(KeyCode::Up));
|
|
assert_eq!(app.scroll_offset, 1);
|
|
app.handle_key(key(KeyCode::Up));
|
|
assert_eq!(app.scroll_offset, 2);
|
|
app.handle_key(key(KeyCode::Down));
|
|
assert_eq!(app.scroll_offset, 1);
|
|
app.handle_key(key(KeyCode::Down));
|
|
assert_eq!(app.scroll_offset, 0);
|
|
// Should not go below 0
|
|
app.handle_key(key(KeyCode::Down));
|
|
assert_eq!(app.scroll_offset, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn ctrl_c_not_handled_in_tui_layer() {
|
|
// Ctrl+C is handled at the signal level (tokio::signal::ctrl_c),
|
|
// not in TUI key handling. Crossterm delivers Char('c') with CONTROL modifier,
|
|
// which the TUI treats like any other char input.
|
|
let mut app = make_app();
|
|
app.handle_key(key(KeyCode::Char('h')));
|
|
app.handle_key(key(KeyCode::Char('i')));
|
|
// Ctrl+C in editing mode inserts 'c' (crossterm behavior)
|
|
app.handle_key(ctrl_key(KeyCode::Char('c')));
|
|
assert_eq!(app.input, "hic");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Chat state tests
|
|
// ============================================================================
|
|
mod chat_tests {
|
|
use p2p_chat::chat::ChatState;
|
|
use p2p_chat::protocol::ChatMessage;
|
|
|
|
#[test]
|
|
fn new_chat_state() {
|
|
let chat = ChatState::new("alice".into());
|
|
assert_eq!(chat.our_name, "alice");
|
|
assert!(chat.history.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn add_system_message() {
|
|
let mut chat = ChatState::new("alice".into());
|
|
chat.add_system_message("test message".into());
|
|
assert_eq!(chat.history.len(), 1);
|
|
assert!(chat.history[0].is_system);
|
|
assert_eq!(chat.history[0].text, "test message");
|
|
assert_eq!(chat.history[0].sender_name, "SYSTEM");
|
|
}
|
|
|
|
#[test]
|
|
fn receive_message() {
|
|
let mut chat = ChatState::new("alice".into());
|
|
chat.receive_message(ChatMessage {
|
|
sender_name: "bob".into(),
|
|
timestamp: 1234567890,
|
|
text: "hello".into(),
|
|
});
|
|
assert_eq!(chat.history.len(), 1);
|
|
assert!(!chat.history[0].is_self);
|
|
assert!(!chat.history[0].is_system);
|
|
assert_eq!(chat.history[0].sender_name, "bob");
|
|
assert_eq!(chat.history[0].text, "hello");
|
|
}
|
|
|
|
#[test]
|
|
fn history_trimming() {
|
|
let mut chat = ChatState::new("alice".into());
|
|
chat.max_history = 5;
|
|
for i in 0..10 {
|
|
chat.add_system_message(format!("msg {}", i));
|
|
}
|
|
assert_eq!(chat.history.len(), 5);
|
|
// Should keep the last 5 messages
|
|
assert_eq!(chat.history[0].text, "msg 5");
|
|
assert_eq!(chat.history[4].text, "msg 9");
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_message_types() {
|
|
let mut chat = ChatState::new("alice".into());
|
|
chat.add_system_message("welcome".into());
|
|
chat.receive_message(ChatMessage {
|
|
sender_name: "bob".into(),
|
|
timestamp: 100,
|
|
text: "hi alice".into(),
|
|
});
|
|
chat.add_system_message("bob left".into());
|
|
|
|
assert_eq!(chat.history.len(), 3);
|
|
assert!(chat.history[0].is_system);
|
|
assert!(!chat.history[1].is_system);
|
|
assert_eq!(chat.history[1].sender_name, "bob");
|
|
assert!(chat.history[2].is_system);
|
|
}
|
|
}
|