Skip to main content

hojicha_core/testing/
mod.rs

1//! Testing utilities for hojicha applications
2//!
3//! This module provides tools for testing TUI applications in headless mode.
4
5pub mod event_recorder;
6pub mod event_test_harness;
7pub mod test_backend;
8pub mod time_control;
9pub mod unified_harness;
10
11pub use event_recorder::{EventRecorder, RecordedEvent};
12pub use event_test_harness::{EventTestHarness, PriorityEventTestHarness};
13pub use test_backend::TestBackend;
14pub use time_control::*;
15pub use unified_harness::*;
16
17use crate::{
18    core::{Cmd, Model},
19    event::Event,
20};
21use std::sync::{Arc, Mutex};
22
23/// Test harness for running models in headless mode
24pub struct TestHarness<M: Model> {
25    model: M,
26    events: Vec<Event<M::Message>>,
27    outputs: Arc<Mutex<Vec<String>>>,
28}
29
30impl<M: Model> TestHarness<M> {
31    /// Create a new test harness
32    pub fn new(model: M) -> Self {
33        Self {
34            model,
35            events: Vec::new(),
36            outputs: Arc::new(Mutex::new(Vec::new())),
37        }
38    }
39
40    /// Add an event to be processed
41    pub fn send_event(mut self, event: Event<M::Message>) -> Self {
42        self.events.push(event);
43        self
44    }
45
46    /// Add multiple events
47    pub fn send_events(mut self, events: Vec<Event<M::Message>>) -> Self {
48        self.events.extend(events);
49        self
50    }
51
52    /// Run the test and collect outputs
53    pub fn run(mut self) -> TestResult<M>
54    where
55        M::Message: std::fmt::Debug,
56        Cmd<M::Message>: std::fmt::Debug,
57    {
58        let outputs = Arc::clone(&self.outputs);
59        let mut commands_executed = Vec::new();
60
61        // Run init
62        let cmd = self.model.init();
63        if !cmd.is_noop() {
64            commands_executed.push(format!("Init command: {cmd:?}"));
65        }
66
67        // Process each event
68        for event in self.events {
69            let event_str = format!("{event:?}");
70            let cmd = self.model.update(event);
71            if !cmd.is_noop() {
72                commands_executed.push(format!("Command from {event_str}: {cmd:?}"));
73            }
74        }
75
76        let final_outputs = outputs.lock().unwrap().clone();
77        TestResult {
78            model: self.model,
79            outputs: final_outputs,
80            commands_executed,
81        }
82    }
83}
84
85/// Result of a test run
86pub struct TestResult<M: Model> {
87    /// The final state of the model after test execution
88    pub model: M,
89    /// All view outputs produced during the test
90    pub outputs: Vec<String>,
91    /// Names of all commands that were executed
92    pub commands_executed: Vec<String>,
93}
94
95impl<M: Model> TestResult<M> {
96    /// Assert that a specific output was produced
97    pub fn assert_output_contains(&self, expected: &str) -> &Self {
98        assert!(
99            self.outputs.iter().any(|o| o.contains(expected)),
100            "Expected output to contain '{}', but got: {:?}",
101            expected,
102            self.outputs
103        );
104        self
105    }
106
107    /// Assert that a command was executed
108    pub fn assert_command_executed(&self, command_substr: &str) -> &Self {
109        assert!(
110            self.commands_executed
111                .iter()
112                .any(|c| c.contains(command_substr)),
113            "Expected command containing '{}' to be executed, but got: {:?}",
114            command_substr,
115            self.commands_executed
116        );
117        self
118    }
119
120    /// Get the final model state
121    pub fn into_model(self) -> M {
122        self.model
123    }
124}
125
126/// Macro for easily creating test events
127#[macro_export]
128macro_rules! test_events {
129    ($($event:expr),* $(,)?) => {
130        vec![$($event),*]
131    };
132}
133
134/// Macro for key events
135#[macro_export]
136macro_rules! key {
137    (Char($ch:expr)) => {
138        Event::Key($crate::event::KeyEvent::new(
139            $crate::event::Key::Char($ch),
140            $crate::event::KeyModifiers::empty(),
141        ))
142    };
143    (Enter) => {
144        Event::Key($crate::event::KeyEvent::new(
145            $crate::event::Key::Enter,
146            $crate::event::KeyModifiers::empty(),
147        ))
148    };
149    (Esc) => {
150        Event::Key($crate::event::KeyEvent::new(
151            $crate::event::Key::Esc,
152            $crate::event::KeyModifiers::empty(),
153        ))
154    };
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    struct TestModel {
162        counter: i32,
163    }
164
165    impl Model for TestModel {
166        type Message = i32;
167
168        fn init(&mut self) -> Cmd<Self::Message> {
169            Cmd::noop()
170        }
171
172        fn update(&mut self, event: Event<Self::Message>) -> Cmd<Self::Message> {
173            if let Some(n) = event.as_user() {
174                self.counter += *n;
175            }
176            Cmd::noop()
177        }
178
179        fn view(&self) -> String {
180            format!("Counter: {}", self.counter)
181        }
182    }
183
184    #[test]
185    fn test_harness_basic() {
186        let model = TestModel { counter: 0 };
187
188        let result = TestHarness::new(model)
189            .send_event(Event::User(5))
190            .send_event(Event::User(3))
191            .run();
192
193        assert_eq!(result.model.counter, 8);
194    }
195}