//! Mock implementation of [`Prompter`] for testing //! //! This module is gated via `#[cfg(any(test, feature = "test-utils"))]` on its //! declaration in `mod.rs`, so it is never compiled into production binaries. //! //! [`Prompter`]: super::prompter::Prompter use std::sync::{Arc, Mutex}; use crate::{ commit::types::{BreakingChange, CommitType, Description, Scope}, error::Error, prompts::prompter::Prompter, }; /// Enum representing different types of mock responses #[derive(Debug)] enum MockResponse { CommitType(CommitType), Scope(Scope), Description(Description), BreakingChange(BreakingChange), Confirm(bool), Error(Error), } /// Mock implementation of [`Prompter`] for testing /// /// This struct allows configuring responses for each prompt type and tracks /// which prompts were called during test execution. #[derive(Debug, Default, Clone)] pub struct MockPrompts { /// Queue of responses to return for each prompt call responses: Arc>>, /// Track which prompts were called (for verification) prompts_called: Arc>>, /// Messages emitted via emit_message() for test assertion messages: Arc>>, } impl MockPrompts { /// Create a new MockPrompts with empty response queue pub fn new() -> Self { Self::default() } /// Configure the mock to return a specific commit type pub fn with_commit_type(self, commit_type: CommitType) -> Self { self.responses .lock() .unwrap() .push(MockResponse::CommitType(commit_type)); self } /// Configure the mock to return a specific scope pub fn with_scope(self, scope: Scope) -> Self { self.responses .lock() .unwrap() .push(MockResponse::Scope(scope)); self } /// Configure the mock to return a specific description pub fn with_description(self, description: Description) -> Self { self.responses .lock() .unwrap() .push(MockResponse::Description(description)); self } /// Configure the mock to return a specific breaking change response pub fn with_breaking_change(self, breaking_change: BreakingChange) -> Self { self.responses .lock() .unwrap() .push(MockResponse::BreakingChange(breaking_change)); self } /// Configure the mock to return a specific confirmation response pub fn with_confirm(self, confirm: bool) -> Self { self.responses .lock() .unwrap() .push(MockResponse::Confirm(confirm)); self } /// Configure the mock to return an error pub fn with_error(self, error: Error) -> Self { self.responses .lock() .unwrap() .push(MockResponse::Error(error)); self } /// Check if select_commit_type was called pub fn was_commit_type_called(&self) -> bool { self.prompts_called .lock() .unwrap() .contains(&"select_commit_type".to_string()) } /// Check if input_scope was called pub fn was_scope_called(&self) -> bool { self.prompts_called .lock() .unwrap() .contains(&"input_scope".to_string()) } /// Check if input_description was called pub fn was_description_called(&self) -> bool { self.prompts_called .lock() .unwrap() .contains(&"input_description".to_string()) } /// Check if input_breaking_change was called pub fn was_breaking_change_called(&self) -> bool { self.prompts_called .lock() .unwrap() .contains(&"input_breaking_change".to_string()) } /// Check if confirm_apply was called pub fn was_confirm_called(&self) -> bool { self.prompts_called .lock() .unwrap() .contains(&"confirm_apply".to_string()) } /// Get all messages emitted via emit_message() pub fn emitted_messages(&self) -> Vec { self.messages.lock().unwrap().clone() } } impl Prompter for MockPrompts { fn select_commit_type(&self) -> Result { self.prompts_called .lock() .unwrap() .push("select_commit_type".to_string()); match self.responses.lock().unwrap().remove(0) { MockResponse::CommitType(ct) => Ok(ct), MockResponse::Error(e) => Err(e), _ => panic!("MockPrompts: Expected CommitType response, got different type"), } } fn input_scope(&self) -> Result { self.prompts_called .lock() .unwrap() .push("input_scope".to_string()); match self.responses.lock().unwrap().remove(0) { MockResponse::Scope(scope) => Ok(scope), MockResponse::Error(e) => Err(e), _ => panic!("MockPrompts: Expected Scope response, got different type"), } } fn input_description(&self) -> Result { self.prompts_called .lock() .unwrap() .push("input_description".to_string()); match self.responses.lock().unwrap().remove(0) { MockResponse::Description(desc) => Ok(desc), MockResponse::Error(e) => Err(e), _ => panic!("MockPrompts: Expected Description response, got different type"), } } fn input_breaking_change(&self) -> Result { self.prompts_called .lock() .unwrap() .push("input_breaking_change".to_string()); match self.responses.lock().unwrap().remove(0) { MockResponse::BreakingChange(bc) => Ok(bc), MockResponse::Error(e) => Err(e), _ => panic!("MockPrompts: Expected BreakingChange response, got different type"), } } fn confirm_apply(&self, _message: &str) -> Result { self.prompts_called .lock() .unwrap() .push("confirm_apply".to_string()); match self.responses.lock().unwrap().remove(0) { MockResponse::Confirm(confirm) => Ok(confirm), MockResponse::Error(e) => Err(e), _ => panic!("MockPrompts: Expected Confirm response, got different type"), } } fn emit_message(&self, msg: &str) { self.messages.lock().unwrap().push(msg.to_string()); } } #[cfg(test)] mod tests { use super::*; use crate::commit::types::{CommitType, Description, Scope}; #[test] fn mock_prompts_creation() { let mock = MockPrompts::new(); assert!(matches!(mock, MockPrompts { .. })); } #[test] fn mock_prompts_implements_trait() { let mock = MockPrompts::new(); fn _accepts_prompter(_p: impl Prompter) {} _accepts_prompter(mock); } #[test] fn mock_select_commit_type() { let mock = MockPrompts::new().with_commit_type(CommitType::Feat); let result = mock.select_commit_type(); assert!(result.is_ok()); assert_eq!(result.unwrap(), CommitType::Feat); assert!(mock.was_commit_type_called()); } #[test] fn mock_input_scope() { let scope = Scope::parse("test-scope").unwrap(); let mock = MockPrompts::new().with_scope(scope.clone()); let result = mock.input_scope(); assert!(result.is_ok()); assert_eq!(result.unwrap(), scope); assert!(mock.was_scope_called()); } #[test] fn mock_input_description() { let desc = Description::parse("test description").unwrap(); let mock = MockPrompts::new().with_description(desc.clone()); let result = mock.input_description(); assert!(result.is_ok()); assert_eq!(result.unwrap(), desc); assert!(mock.was_description_called()); } #[test] fn mock_confirm_apply() { let mock = MockPrompts::new().with_confirm(true); let result = mock.confirm_apply("test message"); assert!(result.is_ok()); assert!(result.unwrap()); assert!(mock.was_confirm_called()); } #[test] fn mock_error_response() { let mock = MockPrompts::new().with_error(Error::Cancelled); let result = mock.select_commit_type(); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), Error::Cancelled)); } #[test] fn mock_tracks_prompt_calls() { let mock = MockPrompts::new() .with_commit_type(CommitType::Fix) .with_scope(Scope::empty()) .with_description(Description::parse("test").unwrap()) .with_confirm(true); mock.select_commit_type().unwrap(); mock.input_scope().unwrap(); mock.input_description().unwrap(); mock.confirm_apply("test").unwrap(); assert!(mock.was_commit_type_called()); assert!(mock.was_scope_called()); assert!(mock.was_description_called()); assert!(mock.was_confirm_called()); } #[test] fn mock_emit_message_records_messages() { let mock = MockPrompts::new(); mock.emit_message("hello"); mock.emit_message("world"); let msgs = mock.emitted_messages(); assert_eq!(msgs, vec!["hello", "world"]); } #[test] fn mock_emit_message_starts_empty() { let mock = MockPrompts::new(); assert!(mock.emitted_messages().is_empty()); } #[test] fn mock_input_breaking_change_no() { let mock = MockPrompts::new().with_breaking_change(BreakingChange::No); let result = mock.input_breaking_change(); assert!(result.is_ok()); assert_eq!(result.unwrap(), BreakingChange::No); assert!(mock.was_breaking_change_called()); } #[test] fn mock_input_breaking_change_yes_no_note() { let mock = MockPrompts::new().with_breaking_change(BreakingChange::Yes); let result = mock.input_breaking_change(); assert!(result.is_ok()); assert_eq!(result.unwrap(), BreakingChange::Yes); assert!(mock.was_breaking_change_called()); } #[test] fn mock_input_breaking_change_yes_with_note() { let mock = MockPrompts::new().with_breaking_change("removes old API".into()); let result = mock.input_breaking_change(); assert!(result.is_ok()); assert_eq!( result.unwrap(), BreakingChange::WithNote("removes old API".into()) ); assert!(mock.was_breaking_change_called()); } #[test] fn mock_input_breaking_change_error() { let mock = MockPrompts::new().with_error(Error::Cancelled); let result = mock.input_breaking_change(); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), Error::Cancelled)); } #[test] fn mock_tracks_breaking_change_call() { let mock = MockPrompts::new() .with_commit_type(CommitType::Fix) .with_scope(Scope::empty()) .with_description(Description::parse("test").unwrap()) .with_breaking_change(BreakingChange::No) .with_confirm(true); mock.select_commit_type().unwrap(); mock.input_scope().unwrap(); mock.input_description().unwrap(); mock.input_breaking_change().unwrap(); mock.confirm_apply("test").unwrap(); assert!(mock.was_commit_type_called()); assert!(mock.was_scope_called()); assert!(mock.was_description_called()); assert!(mock.was_breaking_change_called()); assert!(mock.was_confirm_called()); } }