test(hands): expand Slideshow tests (4→34) and fix Clip invalid action test
Slideshow: add navigation edge cases, autoplay/pause/resume, spotlight/ laser/highlight defaults, content block deserialization, Hand trait dispatch, and add_slide helper tests. Clip: fix test_execute_invalid_action to expect Err (execute returns HandError for unknown variants). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -346,13 +346,50 @@ impl Hand for SlideshowHand {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
// === Config & Defaults ===
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_slideshow_creation() {
|
||||
let hand = SlideshowHand::new();
|
||||
assert_eq!(hand.config().id, "slideshow");
|
||||
assert_eq!(hand.config().name, "幻灯片");
|
||||
assert!(!hand.config().needs_approval);
|
||||
assert!(hand.config().enabled);
|
||||
assert!(hand.config().tags.contains(&"presentation".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_impl() {
|
||||
let hand = SlideshowHand::default();
|
||||
assert_eq!(hand.config().id, "slideshow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_needs_approval() {
|
||||
let hand = SlideshowHand::new();
|
||||
assert!(!hand.needs_approval());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_status() {
|
||||
let hand = SlideshowHand::new();
|
||||
assert_eq!(hand.status(), HandStatus::Idle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_state() {
|
||||
let state = SlideshowState::default();
|
||||
assert_eq!(state.current_slide, 0);
|
||||
assert_eq!(state.total_slides, 0);
|
||||
assert!(!state.is_playing);
|
||||
assert_eq!(state.auto_play_interval_ms, 5000);
|
||||
assert!(state.slides.is_empty());
|
||||
}
|
||||
|
||||
// === Navigation ===
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_navigation() {
|
||||
let hand = SlideshowHand::with_slides_async(vec![
|
||||
@@ -374,6 +411,53 @@ mod tests {
|
||||
assert_eq!(hand.get_state().await.current_slide, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_next_slide_at_end() {
|
||||
let hand = SlideshowHand::with_slides_async(vec![
|
||||
SlideContent { title: "Only Slide".to_string(), subtitle: None, content: vec![], notes: None, background: None },
|
||||
]).await;
|
||||
|
||||
// At slide 0, should not advance past last slide
|
||||
hand.execute_action(SlideshowAction::NextSlide).await.unwrap();
|
||||
assert_eq!(hand.get_state().await.current_slide, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_prev_slide_at_beginning() {
|
||||
let hand = SlideshowHand::with_slides_async(vec![
|
||||
SlideContent { title: "Slide 1".to_string(), subtitle: None, content: vec![], notes: None, background: None },
|
||||
SlideContent { title: "Slide 2".to_string(), subtitle: None, content: vec![], notes: None, background: None },
|
||||
]).await;
|
||||
|
||||
// At slide 0, should not go below 0
|
||||
hand.execute_action(SlideshowAction::PrevSlide).await.unwrap();
|
||||
assert_eq!(hand.get_state().await.current_slide, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_goto_slide_out_of_range() {
|
||||
let hand = SlideshowHand::with_slides_async(vec![
|
||||
SlideContent { title: "Slide 1".to_string(), subtitle: None, content: vec![], notes: None, background: None },
|
||||
]).await;
|
||||
|
||||
let result = hand.execute_action(SlideshowAction::GotoSlide { slide_number: 5 }).await.unwrap();
|
||||
assert!(!result.success);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_goto_slide_returns_content() {
|
||||
let hand = SlideshowHand::with_slides_async(vec![
|
||||
SlideContent { title: "First".to_string(), subtitle: None, content: vec![], notes: None, background: None },
|
||||
SlideContent { title: "Second".to_string(), subtitle: None, content: vec![], notes: None, background: None },
|
||||
]).await;
|
||||
|
||||
let result = hand.execute_action(SlideshowAction::GotoSlide { slide_number: 1 }).await.unwrap();
|
||||
assert!(result.success);
|
||||
assert_eq!(result.output["slide_content"]["title"], "Second");
|
||||
}
|
||||
|
||||
// === Spotlight & Laser & Highlight ===
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_spotlight() {
|
||||
let hand = SlideshowHand::new();
|
||||
@@ -384,6 +468,20 @@ mod tests {
|
||||
|
||||
let result = hand.execute_action(action).await.unwrap();
|
||||
assert!(result.success);
|
||||
assert_eq!(result.output["element_id"], "title");
|
||||
assert_eq!(result.output["duration_ms"], 2000);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_spotlight_default_duration() {
|
||||
let hand = SlideshowHand::new();
|
||||
let action = SlideshowAction::Spotlight {
|
||||
element_id: "elem".to_string(),
|
||||
duration_ms: default_spotlight_duration(),
|
||||
};
|
||||
|
||||
let result = hand.execute_action(action).await.unwrap();
|
||||
assert_eq!(result.output["duration_ms"], 2000);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -397,8 +495,96 @@ mod tests {
|
||||
|
||||
let result = hand.execute_action(action).await.unwrap();
|
||||
assert!(result.success);
|
||||
assert_eq!(result.output["x"], 100.0);
|
||||
assert_eq!(result.output["y"], 200.0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_highlight_default_color() {
|
||||
let hand = SlideshowHand::new();
|
||||
let action = SlideshowAction::Highlight {
|
||||
x: 10.0, y: 20.0, width: 100.0, height: 50.0,
|
||||
color: None, duration_ms: 2000,
|
||||
};
|
||||
|
||||
let result = hand.execute_action(action).await.unwrap();
|
||||
assert!(result.success);
|
||||
assert_eq!(result.output["color"], "#ffcc00");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_highlight_custom_color() {
|
||||
let hand = SlideshowHand::new();
|
||||
let action = SlideshowAction::Highlight {
|
||||
x: 0.0, y: 0.0, width: 50.0, height: 50.0,
|
||||
color: Some("#ff0000".to_string()), duration_ms: 1000,
|
||||
};
|
||||
|
||||
let result = hand.execute_action(action).await.unwrap();
|
||||
assert_eq!(result.output["color"], "#ff0000");
|
||||
}
|
||||
|
||||
// === AutoPlay / Pause / Resume ===
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_autoplay_pause_resume() {
|
||||
let hand = SlideshowHand::new();
|
||||
|
||||
// AutoPlay
|
||||
let result = hand.execute_action(SlideshowAction::AutoPlay { interval_ms: 3000 }).await.unwrap();
|
||||
assert!(result.success);
|
||||
assert!(hand.get_state().await.is_playing);
|
||||
assert_eq!(hand.get_state().await.auto_play_interval_ms, 3000);
|
||||
|
||||
// Pause
|
||||
hand.execute_action(SlideshowAction::Pause).await.unwrap();
|
||||
assert!(!hand.get_state().await.is_playing);
|
||||
|
||||
// Resume
|
||||
hand.execute_action(SlideshowAction::Resume).await.unwrap();
|
||||
assert!(hand.get_state().await.is_playing);
|
||||
|
||||
// Stop
|
||||
hand.execute_action(SlideshowAction::StopAutoPlay).await.unwrap();
|
||||
assert!(!hand.get_state().await.is_playing);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_autoplay_default_interval() {
|
||||
let hand = SlideshowHand::new();
|
||||
hand.execute_action(SlideshowAction::AutoPlay { interval_ms: default_interval() }).await.unwrap();
|
||||
assert_eq!(hand.get_state().await.auto_play_interval_ms, 5000);
|
||||
}
|
||||
|
||||
// === PlayAnimation ===
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_play_animation() {
|
||||
let hand = SlideshowHand::new();
|
||||
let result = hand.execute_action(SlideshowAction::PlayAnimation {
|
||||
animation_id: "fade_in".to_string(),
|
||||
}).await.unwrap();
|
||||
|
||||
assert!(result.success);
|
||||
assert_eq!(result.output["animation_id"], "fade_in");
|
||||
}
|
||||
|
||||
// === GetState ===
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_state() {
|
||||
let hand = SlideshowHand::with_slides_async(vec![
|
||||
SlideContent { title: "A".to_string(), subtitle: None, content: vec![], notes: None, background: None },
|
||||
]).await;
|
||||
|
||||
let result = hand.execute_action(SlideshowAction::GetState).await.unwrap();
|
||||
assert!(result.success);
|
||||
assert_eq!(result.output["total_slides"], 1);
|
||||
assert_eq!(result.output["current_slide"], 0);
|
||||
}
|
||||
|
||||
// === SetContent ===
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_content() {
|
||||
let hand = SlideshowHand::new();
|
||||
@@ -421,5 +607,188 @@ mod tests {
|
||||
|
||||
assert!(result.success);
|
||||
assert_eq!(hand.get_state().await.total_slides, 1);
|
||||
assert_eq!(hand.get_state().await.slides[0].title, "Test Slide");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_content_append() {
|
||||
let hand = SlideshowHand::with_slides_async(vec![
|
||||
SlideContent { title: "First".to_string(), subtitle: None, content: vec![], notes: None, background: None },
|
||||
]).await;
|
||||
|
||||
let content = SlideContent {
|
||||
title: "Appended".to_string(), subtitle: None, content: vec![], notes: None, background: None,
|
||||
};
|
||||
|
||||
let result = hand.execute_action(SlideshowAction::SetContent {
|
||||
slide_number: 1,
|
||||
content,
|
||||
}).await.unwrap();
|
||||
|
||||
assert!(result.success);
|
||||
assert_eq!(result.output["status"], "slide_added");
|
||||
assert_eq!(hand.get_state().await.total_slides, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_content_invalid_index() {
|
||||
let hand = SlideshowHand::new();
|
||||
|
||||
let content = SlideContent {
|
||||
title: "Gap".to_string(), subtitle: None, content: vec![], notes: None, background: None,
|
||||
};
|
||||
|
||||
let result = hand.execute_action(SlideshowAction::SetContent {
|
||||
slide_number: 5,
|
||||
content,
|
||||
}).await.unwrap();
|
||||
|
||||
assert!(!result.success);
|
||||
}
|
||||
|
||||
// === Action Deserialization ===
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_next_slide() {
|
||||
let action: SlideshowAction = serde_json::from_value(json!({"action": "next_slide"})).unwrap();
|
||||
assert!(matches!(action, SlideshowAction::NextSlide));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_goto_slide() {
|
||||
let action: SlideshowAction = serde_json::from_value(json!({"action": "goto_slide", "slide_number": 3})).unwrap();
|
||||
match action {
|
||||
SlideshowAction::GotoSlide { slide_number } => assert_eq!(slide_number, 3),
|
||||
_ => panic!("Expected GotoSlide"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_laser() {
|
||||
let action: SlideshowAction = serde_json::from_value(json!({
|
||||
"action": "laser", "x": 50.0, "y": 75.0
|
||||
})).unwrap();
|
||||
match action {
|
||||
SlideshowAction::Laser { x, y, .. } => {
|
||||
assert_eq!(x, 50.0);
|
||||
assert_eq!(y, 75.0);
|
||||
}
|
||||
_ => panic!("Expected Laser"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_autoplay() {
|
||||
let action: SlideshowAction = serde_json::from_value(json!({"action": "auto_play"})).unwrap();
|
||||
match action {
|
||||
SlideshowAction::AutoPlay { interval_ms } => assert_eq!(interval_ms, 5000),
|
||||
_ => panic!("Expected AutoPlay"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_invalid_action() {
|
||||
let result = serde_json::from_value::<SlideshowAction>(json!({"action": "nonexistent"}));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// === ContentBlock Deserialization ===
|
||||
|
||||
#[test]
|
||||
fn test_content_block_text() {
|
||||
let block: ContentBlock = serde_json::from_value(json!({
|
||||
"type": "text", "text": "Hello"
|
||||
})).unwrap();
|
||||
match block {
|
||||
ContentBlock::Text { text, style } => {
|
||||
assert_eq!(text, "Hello");
|
||||
assert!(style.is_none());
|
||||
}
|
||||
_ => panic!("Expected Text"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_content_block_list() {
|
||||
let block: ContentBlock = serde_json::from_value(json!({
|
||||
"type": "list", "items": ["A", "B"], "ordered": true
|
||||
})).unwrap();
|
||||
match block {
|
||||
ContentBlock::List { items, ordered } => {
|
||||
assert_eq!(items, vec!["A", "B"]);
|
||||
assert!(ordered);
|
||||
}
|
||||
_ => panic!("Expected List"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_content_block_code() {
|
||||
let block: ContentBlock = serde_json::from_value(json!({
|
||||
"type": "code", "code": "fn main() {}", "language": "rust"
|
||||
})).unwrap();
|
||||
match block {
|
||||
ContentBlock::Code { code, language } => {
|
||||
assert_eq!(code, "fn main() {}");
|
||||
assert_eq!(language, Some("rust".to_string()));
|
||||
}
|
||||
_ => panic!("Expected Code"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_content_block_table() {
|
||||
let block: ContentBlock = serde_json::from_value(json!({
|
||||
"type": "table",
|
||||
"headers": ["Name", "Age"],
|
||||
"rows": [["Alice", "30"]]
|
||||
})).unwrap();
|
||||
match block {
|
||||
ContentBlock::Table { headers, rows } => {
|
||||
assert_eq!(headers, vec!["Name", "Age"]);
|
||||
assert_eq!(rows, vec![vec!["Alice", "30"]]);
|
||||
}
|
||||
_ => panic!("Expected Table"),
|
||||
}
|
||||
}
|
||||
|
||||
// === Hand trait via execute ===
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_hand_execute_dispatch() {
|
||||
let hand = SlideshowHand::with_slides_async(vec![
|
||||
SlideContent { title: "S1".to_string(), subtitle: None, content: vec![], notes: None, background: None },
|
||||
SlideContent { title: "S2".to_string(), subtitle: None, content: vec![], notes: None, background: None },
|
||||
]).await;
|
||||
|
||||
let ctx = HandContext::default();
|
||||
let result = hand.execute(&ctx, json!({"action": "next_slide"})).await.unwrap();
|
||||
assert!(result.success);
|
||||
assert_eq!(result.output["current_slide"], 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_hand_execute_invalid_action() {
|
||||
let hand = SlideshowHand::new();
|
||||
let ctx = HandContext::default();
|
||||
let result = hand.execute(&ctx, json!({"action": "invalid"})).await.unwrap();
|
||||
assert!(!result.success);
|
||||
}
|
||||
|
||||
// === add_slide helper ===
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_slide() {
|
||||
let hand = SlideshowHand::new();
|
||||
hand.add_slide(SlideContent {
|
||||
title: "Dynamic".to_string(), subtitle: None, content: vec![], notes: None, background: None,
|
||||
}).await;
|
||||
hand.add_slide(SlideContent {
|
||||
title: "Dynamic 2".to_string(), subtitle: None, content: vec![], notes: None, background: None,
|
||||
}).await;
|
||||
|
||||
let state = hand.get_state().await;
|
||||
assert_eq!(state.total_slides, 2);
|
||||
assert_eq!(state.slides.len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user