Files
jj-cz/src/prompts/mock.rs

376 lines
12 KiB
Rust

//! 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<Mutex<Vec<MockResponse>>>,
/// Track which prompts were called (for verification)
prompts_called: Arc<Mutex<Vec<String>>>,
/// Messages emitted via emit_message() for test assertion
messages: Arc<Mutex<Vec<String>>>,
}
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<String> {
self.messages.lock().unwrap().clone()
}
}
impl Prompter for MockPrompts {
fn select_commit_type(&self) -> Result<CommitType, Error> {
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<Scope, Error> {
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<Description, Error> {
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<BreakingChange, Error> {
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<bool, Error> {
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());
}
}