pub mod event_recorder;
pub mod event_test_harness;
pub mod test_backend;
pub mod time_control;
pub mod unified_harness;
pub use event_recorder::{EventRecorder, RecordedEvent};
pub use event_test_harness::{EventTestHarness, PriorityEventTestHarness};
pub use test_backend::TestBackend;
pub use time_control::*;
pub use unified_harness::*;
use crate::{
core::{Cmd, Model},
event::Event,
};
use std::sync::{Arc, Mutex};
pub struct TestHarness<M: Model> {
model: M,
events: Vec<Event<M::Message>>,
outputs: Arc<Mutex<Vec<String>>>,
}
impl<M: Model> TestHarness<M> {
pub fn new(model: M) -> Self {
Self {
model,
events: Vec::new(),
outputs: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn send_event(mut self, event: Event<M::Message>) -> Self {
self.events.push(event);
self
}
pub fn send_events(mut self, events: Vec<Event<M::Message>>) -> Self {
self.events.extend(events);
self
}
pub fn run(mut self) -> TestResult<M>
where
M::Message: std::fmt::Debug,
Cmd<M::Message>: std::fmt::Debug,
{
let outputs = Arc::clone(&self.outputs);
let mut commands_executed = Vec::new();
let cmd = self.model.init();
if !cmd.is_noop() {
commands_executed.push(format!("Init command: {cmd:?}"));
}
for event in self.events {
let event_str = format!("{event:?}");
let cmd = self.model.update(event);
if !cmd.is_noop() {
commands_executed.push(format!("Command from {event_str}: {cmd:?}"));
}
}
let final_outputs = outputs.lock().unwrap().clone();
TestResult {
model: self.model,
outputs: final_outputs,
commands_executed,
}
}
}
pub struct TestResult<M: Model> {
pub model: M,
pub outputs: Vec<String>,
pub commands_executed: Vec<String>,
}
impl<M: Model> TestResult<M> {
pub fn assert_output_contains(&self, expected: &str) -> &Self {
assert!(
self.outputs.iter().any(|o| o.contains(expected)),
"Expected output to contain '{}', but got: {:?}",
expected,
self.outputs
);
self
}
pub fn assert_command_executed(&self, command_substr: &str) -> &Self {
assert!(
self.commands_executed
.iter()
.any(|c| c.contains(command_substr)),
"Expected command containing '{}' to be executed, but got: {:?}",
command_substr,
self.commands_executed
);
self
}
pub fn into_model(self) -> M {
self.model
}
}
#[macro_export]
macro_rules! test_events {
($($event:expr),* $(,)?) => {
vec![$($event),*]
};
}
#[macro_export]
macro_rules! key {
(Char($ch:expr)) => {
Event::Key($crate::event::KeyEvent::new(
$crate::event::Key::Char($ch),
$crate::event::KeyModifiers::empty(),
))
};
(Enter) => {
Event::Key($crate::event::KeyEvent::new(
$crate::event::Key::Enter,
$crate::event::KeyModifiers::empty(),
))
};
(Esc) => {
Event::Key($crate::event::KeyEvent::new(
$crate::event::Key::Esc,
$crate::event::KeyModifiers::empty(),
))
};
}
#[cfg(test)]
mod tests {
use super::*;
struct TestModel {
counter: i32,
}
impl Model for TestModel {
type Message = i32;
fn init(&mut self) -> Cmd<Self::Message> {
Cmd::noop()
}
fn update(&mut self, event: Event<Self::Message>) -> Cmd<Self::Message> {
if let Some(n) = event.as_user() {
self.counter += *n;
}
Cmd::noop()
}
fn view(&self) -> String {
format!("Counter: {}", self.counter)
}
}
#[test]
fn test_harness_basic() {
let model = TestModel { counter: 0 };
let result = TestHarness::new(model)
.send_event(Event::User(5))
.send_event(Event::User(3))
.run();
assert_eq!(result.model.counter, 8);
}
}