use std::fmt::Debug;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tokio::sync::mpsc;
use crate::event::{ComponentId, Event, EventContext, EventKind};
use crate::keybindings::parse_key_string;
use crate::{Action, ActionCategory};
pub trait ActionAssertions<A> {
fn assert_empty(&self);
fn assert_not_empty(&self);
fn assert_count(&self, n: usize);
fn assert_first_matches<F: Fn(&A) -> bool>(&self, f: F);
fn assert_any_matches<F: Fn(&A) -> bool>(&self, f: F);
fn assert_all_match<F: Fn(&A) -> bool>(&self, f: F);
fn assert_none_match<F: Fn(&A) -> bool>(&self, f: F);
}
pub trait ActionAssertionsEq<A> {
fn assert_first(&self, expected: A);
fn assert_last(&self, expected: A);
fn assert_contains(&self, expected: A);
fn assert_not_contains(&self, expected: A);
}
impl<A: Debug> ActionAssertions<A> for Vec<A> {
fn assert_empty(&self) {
assert!(
self.is_empty(),
"Expected no actions to be emitted, but got: {:?}",
self
);
}
fn assert_not_empty(&self) {
assert!(
!self.is_empty(),
"Expected actions to be emitted, but got none"
);
}
fn assert_count(&self, n: usize) {
assert_eq!(
self.len(),
n,
"Expected {} action(s), got {}: {:?}",
n,
self.len(),
self
);
}
fn assert_first_matches<F: Fn(&A) -> bool>(&self, f: F) {
assert!(
!self.is_empty(),
"Expected first action to match predicate, but no actions were emitted"
);
assert!(
f(&self[0]),
"Expected first action to match predicate, got: {:?}",
self[0]
);
}
fn assert_any_matches<F: Fn(&A) -> bool>(&self, f: F) {
assert!(
self.iter().any(&f),
"Expected any action to match predicate, but none did: {:?}",
self
);
}
fn assert_all_match<F: Fn(&A) -> bool>(&self, f: F) {
for (i, action) in self.iter().enumerate() {
assert!(
f(action),
"Expected all actions to match predicate, but action at index {} didn't: {:?}",
i,
action
);
}
}
fn assert_none_match<F: Fn(&A) -> bool>(&self, f: F) {
for (i, action) in self.iter().enumerate() {
assert!(
!f(action),
"Expected no action to match predicate, but action at index {} matched: {:?}",
i,
action
);
}
}
}
impl<A: PartialEq + Debug> ActionAssertionsEq<A> for Vec<A> {
fn assert_first(&self, expected: A) {
assert!(
!self.is_empty(),
"Expected first action to be {:?}, but no actions were emitted",
expected
);
assert_eq!(
&self[0], &expected,
"Expected first action to be {:?}, got {:?}",
expected, self[0]
);
}
fn assert_last(&self, expected: A) {
assert!(
!self.is_empty(),
"Expected last action to be {:?}, but no actions were emitted",
expected
);
let last = self.last().unwrap();
assert_eq!(
last, &expected,
"Expected last action to be {:?}, got {:?}",
expected, last
);
}
fn assert_contains(&self, expected: A) {
assert!(
self.iter().any(|a| a == &expected),
"Expected actions to contain {:?}, but got: {:?}",
expected,
self
);
}
fn assert_not_contains(&self, expected: A) {
assert!(
!self.iter().any(|a| a == &expected),
"Expected actions NOT to contain {:?}, but it was found in: {:?}",
expected,
self
);
}
}
impl<A: Debug> ActionAssertions<A> for [A] {
fn assert_empty(&self) {
assert!(
self.is_empty(),
"Expected no actions to be emitted, but got: {:?}",
self
);
}
fn assert_not_empty(&self) {
assert!(
!self.is_empty(),
"Expected actions to be emitted, but got none"
);
}
fn assert_count(&self, n: usize) {
assert_eq!(
self.len(),
n,
"Expected {} action(s), got {}: {:?}",
n,
self.len(),
self
);
}
fn assert_first_matches<F: Fn(&A) -> bool>(&self, f: F) {
assert!(
!self.is_empty(),
"Expected first action to match predicate, but no actions were emitted"
);
assert!(
f(&self[0]),
"Expected first action to match predicate, got: {:?}",
self[0]
);
}
fn assert_any_matches<F: Fn(&A) -> bool>(&self, f: F) {
assert!(
self.iter().any(&f),
"Expected any action to match predicate, but none did: {:?}",
self
);
}
fn assert_all_match<F: Fn(&A) -> bool>(&self, f: F) {
for (i, action) in self.iter().enumerate() {
assert!(
f(action),
"Expected all actions to match predicate, but action at index {} didn't: {:?}",
i,
action
);
}
}
fn assert_none_match<F: Fn(&A) -> bool>(&self, f: F) {
for (i, action) in self.iter().enumerate() {
assert!(
!f(action),
"Expected no action to match predicate, but action at index {} matched: {:?}",
i,
action
);
}
}
}
impl<A: PartialEq + Debug> ActionAssertionsEq<A> for [A] {
fn assert_first(&self, expected: A) {
assert!(
!self.is_empty(),
"Expected first action to be {:?}, but no actions were emitted",
expected
);
assert_eq!(
&self[0], &expected,
"Expected first action to be {:?}, got {:?}",
expected, self[0]
);
}
fn assert_last(&self, expected: A) {
assert!(
!self.is_empty(),
"Expected last action to be {:?}, but no actions were emitted",
expected
);
let last = self.last().unwrap();
assert_eq!(
last, &expected,
"Expected last action to be {:?}, got {:?}",
expected, last
);
}
fn assert_contains(&self, expected: A) {
assert!(
self.iter().any(|a| a == &expected),
"Expected actions to contain {:?}, but got: {:?}",
expected,
self
);
}
fn assert_not_contains(&self, expected: A) {
assert!(
!self.iter().any(|a| a == &expected),
"Expected actions NOT to contain {:?}, but it was found in: {:?}",
expected,
self
);
}
}
pub fn key(s: &str) -> KeyEvent {
parse_key_string(s).unwrap_or_else(|| panic!("Invalid key string: {:?}", s))
}
pub fn char_key(c: char) -> KeyEvent {
KeyEvent {
code: KeyCode::Char(c),
modifiers: KeyModifiers::empty(),
kind: crossterm::event::KeyEventKind::Press,
state: crossterm::event::KeyEventState::empty(),
}
}
pub fn ctrl_key(c: char) -> KeyEvent {
KeyEvent {
code: KeyCode::Char(c),
modifiers: KeyModifiers::CONTROL,
kind: crossterm::event::KeyEventKind::Press,
state: crossterm::event::KeyEventState::empty(),
}
}
pub fn alt_key(c: char) -> KeyEvent {
KeyEvent {
code: KeyCode::Char(c),
modifiers: KeyModifiers::ALT,
kind: crossterm::event::KeyEventKind::Press,
state: crossterm::event::KeyEventState::empty(),
}
}
pub fn key_event<C: ComponentId>(s: &str) -> Event<C> {
Event {
kind: EventKind::Key(key(s)),
context: EventContext::default(),
}
}
pub fn into_event<C: ComponentId>(key_event: KeyEvent) -> Event<C> {
Event {
kind: EventKind::Key(key_event),
context: EventContext::default(),
}
}
pub fn key_events<C: ComponentId>(keys: &str) -> Vec<Event<C>> {
keys.split_whitespace().map(|k| key_event::<C>(k)).collect()
}
pub fn keys(key_str: &str) -> Vec<KeyEvent> {
key_str.split_whitespace().map(key).collect()
}
pub struct TestHarness<S, A: Action> {
pub state: S,
tx: mpsc::UnboundedSender<A>,
rx: mpsc::UnboundedReceiver<A>,
}
impl<S, A: Action> TestHarness<S, A> {
pub fn new(state: S) -> Self {
let (tx, rx) = mpsc::unbounded_channel();
Self { state, tx, rx }
}
pub fn sender(&self) -> mpsc::UnboundedSender<A> {
self.tx.clone()
}
pub fn emit(&self, action: A) {
let _ = self.tx.send(action);
}
pub fn drain_emitted(&mut self) -> Vec<A> {
let mut actions = Vec::new();
while let Ok(action) = self.rx.try_recv() {
actions.push(action);
}
actions
}
pub fn has_emitted(&mut self) -> bool {
!self.drain_emitted().is_empty()
}
pub fn complete_action(&self, action: A) {
self.emit(action);
}
pub fn complete_actions(&self, actions: impl IntoIterator<Item = A>) {
for action in actions {
self.emit(action);
}
}
pub fn send_keys<C, H, I>(&mut self, keys: &str, mut handler: H) -> Vec<A>
where
C: ComponentId,
I: IntoIterator<Item = A>,
H: FnMut(&mut S, Event<C>) -> I,
{
let events = key_events::<C>(keys);
let mut all_actions = Vec::new();
for event in events {
let actions = handler(&mut self.state, event);
all_actions.extend(actions);
}
all_actions
}
pub fn send_keys_emit<C, H, I>(&mut self, keys: &str, mut handler: H)
where
C: ComponentId,
I: IntoIterator<Item = A>,
H: FnMut(&mut S, Event<C>) -> I,
{
let events = key_events::<C>(keys);
for event in events {
let actions = handler(&mut self.state, event);
for action in actions {
self.emit(action);
}
}
}
}
impl<S: Default, A: Action> Default for TestHarness<S, A> {
fn default() -> Self {
Self::new(S::default())
}
}
impl<S, A: ActionCategory> TestHarness<S, A> {
pub fn drain_category(&mut self, category: &str) -> Vec<A> {
let all = self.drain_emitted();
let mut matching = Vec::new();
let mut non_matching = Vec::new();
for action in all {
if action.category() == Some(category) {
matching.push(action);
} else {
non_matching.push(action);
}
}
for action in non_matching {
let _ = self.tx.send(action);
}
matching
}
pub fn has_category(&mut self, category: &str) -> bool {
!self.drain_category(category).is_empty()
}
}
#[macro_export]
macro_rules! assert_emitted {
($actions:expr, $pattern:pat $(if $guard:expr)?) => {
assert!(
$actions.iter().any(|a| matches!(a, $pattern $(if $guard)?)),
"Expected action matching `{}` to be emitted, but got: {:?}",
stringify!($pattern),
$actions
);
};
}
#[macro_export]
macro_rules! assert_not_emitted {
($actions:expr, $pattern:pat $(if $guard:expr)?) => {
assert!(
!$actions.iter().any(|a| matches!(a, $pattern $(if $guard)?)),
"Expected action matching `{}` NOT to be emitted, but it was: {:?}",
stringify!($pattern),
$actions
);
};
}
#[macro_export]
macro_rules! find_emitted {
($actions:expr, $pattern:pat $(if $guard:expr)?) => {
$actions.iter().find(|a| matches!(a, $pattern $(if $guard)?))
};
}
#[macro_export]
macro_rules! count_emitted {
($actions:expr, $pattern:pat $(if $guard:expr)?) => {
$actions.iter().filter(|a| matches!(a, $pattern $(if $guard)?)).count()
};
}
#[macro_export]
macro_rules! assert_category_emitted {
($actions:expr, $category:expr) => {
assert!(
$actions.iter().any(|a| {
use $crate::ActionCategory;
a.category() == Some($category)
}),
"Expected action with category `{}` to be emitted, but got: {:?}",
$category,
$actions
);
};
}
#[macro_export]
macro_rules! assert_category_not_emitted {
($actions:expr, $category:expr) => {
assert!(
!$actions.iter().any(|a| {
use $crate::ActionCategory;
a.category() == Some($category)
}),
"Expected NO action with category `{}` to be emitted, but found: {:?}",
$category,
$actions
.iter()
.filter(|a| {
use $crate::ActionCategory;
a.category() == Some($category)
})
.collect::<Vec<_>>()
);
};
}
#[macro_export]
macro_rules! count_category {
($actions:expr, $category:expr) => {{
use $crate::ActionCategory;
$actions
.iter()
.filter(|a| a.category() == Some($category))
.count()
}};
}
#[macro_export]
macro_rules! assert_state {
($harness:expr, $($field:tt).+, $expected:expr) => {
assert_eq!(
$harness.state.$($field).+,
$expected,
"Expected state.{} = {:?}, got {:?}",
stringify!($($field).+),
$expected,
$harness.state.$($field).+
);
};
}
#[macro_export]
macro_rules! assert_state_matches {
($harness:expr, $($field:tt).+, $pattern:pat $(if $guard:expr)?) => {
assert!(
matches!($harness.state.$($field).+, $pattern $(if $guard)?),
"Expected state.{} to match `{}`, got {:?}",
stringify!($($field).+),
stringify!($pattern),
$harness.state.$($field).+
);
};
}
use ratatui::backend::{Backend, TestBackend};
use ratatui::buffer::Buffer;
use ratatui::Terminal;
pub struct RenderHarness {
terminal: Terminal<TestBackend>,
}
impl RenderHarness {
pub fn new(width: u16, height: u16) -> Self {
let backend = TestBackend::new(width, height);
let terminal = Terminal::new(backend).expect("Failed to create test terminal");
Self { terminal }
}
pub fn render<F>(&mut self, render_fn: F) -> &Buffer
where
F: FnOnce(&mut ratatui::Frame),
{
self.terminal
.draw(render_fn)
.expect("Failed to draw to test terminal");
self.terminal.backend().buffer()
}
pub fn render_to_string<F>(&mut self, render_fn: F) -> String
where
F: FnOnce(&mut ratatui::Frame),
{
let buffer = self.render(render_fn);
buffer_to_string(buffer)
}
pub fn render_to_string_plain<F>(&mut self, render_fn: F) -> String
where
F: FnOnce(&mut ratatui::Frame),
{
let buffer = self.render(render_fn);
buffer_to_string_plain(buffer)
}
pub fn size(&self) -> (u16, u16) {
let area = self.terminal.backend().size().unwrap_or_default();
(area.width, area.height)
}
pub fn resize(&mut self, width: u16, height: u16) {
self.terminal.backend_mut().resize(width, height);
}
}
pub fn buffer_to_string(buffer: &Buffer) -> String {
use ratatui::style::{Color, Modifier};
use std::fmt::Write;
let area = buffer.area();
let mut result = String::new();
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
let cell = &buffer[(x, y)];
let _ = write!(result, "\x1b[0m");
match cell.fg {
Color::Reset => {}
Color::Black => result.push_str("\x1b[30m"),
Color::Red => result.push_str("\x1b[31m"),
Color::Green => result.push_str("\x1b[32m"),
Color::Yellow => result.push_str("\x1b[33m"),
Color::Blue => result.push_str("\x1b[34m"),
Color::Magenta => result.push_str("\x1b[35m"),
Color::Cyan => result.push_str("\x1b[36m"),
Color::Gray => result.push_str("\x1b[37m"),
Color::DarkGray => result.push_str("\x1b[90m"),
Color::LightRed => result.push_str("\x1b[91m"),
Color::LightGreen => result.push_str("\x1b[92m"),
Color::LightYellow => result.push_str("\x1b[93m"),
Color::LightBlue => result.push_str("\x1b[94m"),
Color::LightMagenta => result.push_str("\x1b[95m"),
Color::LightCyan => result.push_str("\x1b[96m"),
Color::White => result.push_str("\x1b[97m"),
Color::Rgb(r, g, b) => {
let _ = write!(result, "\x1b[38;2;{};{};{}m", r, g, b);
}
Color::Indexed(i) => {
let _ = write!(result, "\x1b[38;5;{}m", i);
}
}
match cell.bg {
Color::Reset => {}
Color::Black => result.push_str("\x1b[40m"),
Color::Red => result.push_str("\x1b[41m"),
Color::Green => result.push_str("\x1b[42m"),
Color::Yellow => result.push_str("\x1b[43m"),
Color::Blue => result.push_str("\x1b[44m"),
Color::Magenta => result.push_str("\x1b[45m"),
Color::Cyan => result.push_str("\x1b[46m"),
Color::Gray => result.push_str("\x1b[47m"),
Color::DarkGray => result.push_str("\x1b[100m"),
Color::LightRed => result.push_str("\x1b[101m"),
Color::LightGreen => result.push_str("\x1b[102m"),
Color::LightYellow => result.push_str("\x1b[103m"),
Color::LightBlue => result.push_str("\x1b[104m"),
Color::LightMagenta => result.push_str("\x1b[105m"),
Color::LightCyan => result.push_str("\x1b[106m"),
Color::White => result.push_str("\x1b[107m"),
Color::Rgb(r, g, b) => {
let _ = write!(result, "\x1b[48;2;{};{};{}m", r, g, b);
}
Color::Indexed(i) => {
let _ = write!(result, "\x1b[48;5;{}m", i);
}
}
if cell.modifier.contains(Modifier::BOLD) {
result.push_str("\x1b[1m");
}
if cell.modifier.contains(Modifier::DIM) {
result.push_str("\x1b[2m");
}
if cell.modifier.contains(Modifier::ITALIC) {
result.push_str("\x1b[3m");
}
if cell.modifier.contains(Modifier::UNDERLINED) {
result.push_str("\x1b[4m");
}
if cell.modifier.contains(Modifier::REVERSED) {
result.push_str("\x1b[7m");
}
if cell.modifier.contains(Modifier::CROSSED_OUT) {
result.push_str("\x1b[9m");
}
result.push_str(cell.symbol());
}
result.push_str("\x1b[0m\n");
}
result
}
pub fn buffer_to_string_plain(buffer: &Buffer) -> String {
let area = buffer.area();
let mut result = String::new();
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
let cell = &buffer[(x, y)];
result.push_str(cell.symbol());
}
result.push('\n');
}
result
}
pub fn buffer_rect_to_string_plain(buffer: &Buffer, rect: ratatui::layout::Rect) -> String {
let mut result = String::new();
for y in rect.top()..rect.bottom() {
for x in rect.left()..rect.right() {
if x < buffer.area().right() && y < buffer.area().bottom() {
let cell = &buffer[(x, y)];
result.push_str(cell.symbol());
}
}
result.push('\n');
}
result
}
pub struct StoreTestHarness<S, A: Action, E = crate::NoEffect> {
store: crate::Store<S, A, E>,
tx: mpsc::UnboundedSender<A>,
rx: mpsc::UnboundedReceiver<A>,
effects: Vec<E>,
render: Option<RenderHarness>,
default_size: (u16, u16),
}
impl<S, A: Action, E> StoreTestHarness<S, A, E> {
pub fn new(state: S, reducer: crate::Reducer<S, A, E>) -> Self {
let (tx, rx) = mpsc::unbounded_channel();
Self {
store: crate::Store::new(state, reducer),
tx,
rx,
effects: Vec::new(),
render: None,
default_size: (80, 24),
}
}
pub fn with_size(mut self, width: u16, height: u16) -> Self {
self.default_size = (width, height);
self
}
pub fn dispatch(&mut self, action: A) -> crate::ReducerResult<E> {
self.store.dispatch(action)
}
pub fn dispatch_collect(&mut self, action: A) -> bool {
let result = self.store.dispatch(action);
self.effects.extend(result.effects);
result.changed
}
pub fn dispatch_all(&mut self, actions: impl IntoIterator<Item = A>) -> Vec<bool> {
actions
.into_iter()
.map(|a| self.dispatch_collect(a))
.collect()
}
pub fn state(&self) -> &S {
self.store.state()
}
pub fn state_mut(&mut self) -> &mut S {
self.store.state_mut()
}
pub fn drain_effects(&mut self) -> Vec<E> {
std::mem::take(&mut self.effects)
}
pub fn has_effects(&self) -> bool {
!self.effects.is_empty()
}
pub fn effect_count(&self) -> usize {
self.effects.len()
}
pub fn sender(&self) -> mpsc::UnboundedSender<A> {
self.tx.clone()
}
pub fn emit(&self, action: A) {
let _ = self.tx.send(action);
}
pub fn complete_action(&self, action: A) {
self.emit(action);
}
pub fn complete_actions(&self, actions: impl IntoIterator<Item = A>) {
for action in actions {
self.emit(action);
}
}
pub fn drain_emitted(&mut self) -> Vec<A> {
let mut actions = Vec::new();
while let Ok(action) = self.rx.try_recv() {
actions.push(action);
}
actions
}
pub fn has_emitted(&mut self) -> bool {
!self.drain_emitted().is_empty()
}
pub fn process_emitted(&mut self) -> (usize, usize) {
let actions = self.drain_emitted();
let total = actions.len();
let changed = actions
.into_iter()
.filter(|a| self.dispatch_collect(a.clone()))
.count();
(changed, total)
}
pub fn send_keys<C, H, I>(&mut self, keys: &str, mut handler: H) -> Vec<A>
where
C: ComponentId,
I: IntoIterator<Item = A>,
H: FnMut(&mut S, Event<C>) -> I,
{
let events = key_events::<C>(keys);
let mut all_actions = Vec::new();
for event in events {
let actions = handler(self.store.state_mut(), event);
all_actions.extend(actions);
}
all_actions
}
pub fn send_keys_dispatch<C, H, I>(&mut self, keys: &str, mut handler: H) -> Vec<A>
where
C: ComponentId,
I: IntoIterator<Item = A>,
H: FnMut(&mut S, Event<C>) -> I,
{
let events = key_events::<C>(keys);
let mut all_actions = Vec::new();
for event in events {
let actions: Vec<A> = handler(self.store.state_mut(), event).into_iter().collect();
for action in actions {
self.dispatch_collect(action.clone());
all_actions.push(action);
}
}
all_actions
}
pub fn assert_state<F>(&self, predicate: F)
where
F: FnOnce(&S) -> bool,
{
assert!(predicate(self.state()), "State assertion failed");
}
pub fn assert_state_msg<F>(&self, predicate: F, msg: &str)
where
F: FnOnce(&S) -> bool,
{
assert!(predicate(self.state()), "{}", msg);
}
fn ensure_render(&mut self, width: u16, height: u16) {
if self.render.is_none() || self.render.as_ref().map(|r| r.size()) != Some((width, height))
{
self.render = Some(RenderHarness::new(width, height));
}
}
pub fn render<F>(&mut self, width: u16, height: u16, render_fn: F) -> String
where
F: FnOnce(&mut ratatui::Frame, ratatui::layout::Rect, &S),
{
self.ensure_render(width, height);
let store = &self.store;
let render = self.render.as_mut().unwrap();
render.render_to_string(|frame| {
let area = frame.area();
render_fn(frame, area, store.state());
})
}
pub fn render_plain<F>(&mut self, width: u16, height: u16, render_fn: F) -> String
where
F: FnOnce(&mut ratatui::Frame, ratatui::layout::Rect, &S),
{
self.ensure_render(width, height);
let store = &self.store;
let render = self.render.as_mut().unwrap();
render.render_to_string_plain(|frame| {
let area = frame.area();
render_fn(frame, area, store.state());
})
}
pub fn render_default<F>(&mut self, render_fn: F) -> String
where
F: FnOnce(&mut ratatui::Frame, ratatui::layout::Rect, &S),
{
let (w, h) = self.default_size;
self.render(w, h, render_fn)
}
pub fn render_default_plain<F>(&mut self, render_fn: F) -> String
where
F: FnOnce(&mut ratatui::Frame, ratatui::layout::Rect, &S),
{
let (w, h) = self.default_size;
self.render_plain(w, h, render_fn)
}
}
impl<S: Default, A: Action> Default for StoreTestHarness<S, A> {
fn default() -> Self {
Self::new(S::default(), |_, _| crate::ReducerResult::unchanged())
}
}
pub trait EffectAssertions<E> {
fn effects_empty(&self);
fn effects_not_empty(&self);
fn effects_count(&self, n: usize);
fn effects_any_matches<F: Fn(&E) -> bool>(&self, f: F);
fn effects_first_matches<F: Fn(&E) -> bool>(&self, f: F);
fn effects_all_match<F: Fn(&E) -> bool>(&self, f: F);
fn effects_none_match<F: Fn(&E) -> bool>(&self, f: F);
}
impl<E: std::fmt::Debug> EffectAssertions<E> for Vec<E> {
fn effects_empty(&self) {
assert!(self.is_empty(), "Expected no effects, got {:?}", self);
}
fn effects_not_empty(&self) {
assert!(!self.is_empty(), "Expected effects, got none");
}
fn effects_count(&self, n: usize) {
assert_eq!(
self.len(),
n,
"Expected {} effects, got {}: {:?}",
n,
self.len(),
self
);
}
fn effects_any_matches<F: Fn(&E) -> bool>(&self, f: F) {
assert!(
self.iter().any(&f),
"No effect matched predicate in {:?}",
self
);
}
fn effects_first_matches<F: Fn(&E) -> bool>(&self, f: F) {
let first = self.first().expect("Expected at least one effect");
assert!(f(first), "First effect {:?} did not match predicate", first);
}
fn effects_all_match<F: Fn(&E) -> bool>(&self, f: F) {
for (i, e) in self.iter().enumerate() {
assert!(f(e), "Effect at index {} did not match: {:?}", i, e);
}
}
fn effects_none_match<F: Fn(&E) -> bool>(&self, f: F) {
for (i, e) in self.iter().enumerate() {
assert!(!f(e), "Effect at index {} unexpectedly matched: {:?}", i, e);
}
}
}
pub trait EffectAssertionsEq<E> {
fn effects_contains(&self, expected: E);
fn effects_first_eq(&self, expected: E);
fn effects_last_eq(&self, expected: E);
}
impl<E: PartialEq + std::fmt::Debug> EffectAssertionsEq<E> for Vec<E> {
fn effects_contains(&self, expected: E) {
assert!(
self.contains(&expected),
"Expected to contain {:?}, got {:?}",
expected,
self
);
}
fn effects_first_eq(&self, expected: E) {
let first = self.first().expect("Expected at least one effect");
assert_eq!(first, &expected, "First effect mismatch");
}
fn effects_last_eq(&self, expected: E) {
let last = self.last().expect("Expected at least one effect");
assert_eq!(last, &expected, "Last effect mismatch");
}
}
#[cfg(feature = "testing-time")]
pub fn pause_time() {
tokio::time::pause();
}
#[cfg(feature = "testing-time")]
pub fn resume_time() {
tokio::time::resume();
}
#[cfg(feature = "testing-time")]
pub async fn advance_time(duration: std::time::Duration) {
tokio::time::advance(duration).await;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_simple() {
let k = key("q");
assert_eq!(k.code, KeyCode::Char('q'));
assert_eq!(k.modifiers, KeyModifiers::empty());
}
#[test]
fn test_key_with_ctrl() {
let k = key("ctrl+p");
assert_eq!(k.code, KeyCode::Char('p'));
assert!(k.modifiers.contains(KeyModifiers::CONTROL));
}
#[test]
fn test_key_special() {
let k = key("esc");
assert_eq!(k.code, KeyCode::Esc);
let k = key("enter");
assert_eq!(k.code, KeyCode::Enter);
let k = key("shift+tab");
assert_eq!(k.code, KeyCode::BackTab);
}
#[test]
fn test_char_key() {
let k = char_key('x');
assert_eq!(k.code, KeyCode::Char('x'));
assert_eq!(k.modifiers, KeyModifiers::empty());
}
#[test]
fn test_ctrl_key() {
let k = ctrl_key('c');
assert_eq!(k.code, KeyCode::Char('c'));
assert!(k.modifiers.contains(KeyModifiers::CONTROL));
}
#[derive(Clone, Debug, PartialEq)]
enum TestAction {
Foo,
Bar(i32),
}
impl crate::Action for TestAction {
fn name(&self) -> &'static str {
match self {
TestAction::Foo => "Foo",
TestAction::Bar(_) => "Bar",
}
}
}
#[test]
fn test_harness_emit_and_drain() {
let mut harness = TestHarness::<(), TestAction>::new(());
harness.emit(TestAction::Foo);
harness.emit(TestAction::Bar(42));
let actions = harness.drain_emitted();
assert_eq!(actions.len(), 2);
assert_eq!(actions[0], TestAction::Foo);
assert_eq!(actions[1], TestAction::Bar(42));
let actions = harness.drain_emitted();
assert!(actions.is_empty());
}
#[test]
fn test_assert_macros() {
let actions = vec![TestAction::Foo, TestAction::Bar(42)];
assert_emitted!(actions, TestAction::Foo);
assert_emitted!(actions, TestAction::Bar(42));
assert_emitted!(actions, TestAction::Bar(_));
assert_not_emitted!(actions, TestAction::Bar(99));
let found = find_emitted!(actions, TestAction::Bar(_));
assert!(found.is_some());
let count = count_emitted!(actions, TestAction::Bar(_));
assert_eq!(count, 1);
}
#[test]
fn test_action_assertions_first_last() {
let actions = vec![TestAction::Foo, TestAction::Bar(42), TestAction::Bar(99)];
actions.assert_first(TestAction::Foo);
actions.assert_last(TestAction::Bar(99));
}
#[test]
fn test_action_assertions_contains() {
let actions = vec![TestAction::Foo, TestAction::Bar(42)];
actions.assert_contains(TestAction::Foo);
actions.assert_contains(TestAction::Bar(42));
actions.assert_not_contains(TestAction::Bar(99));
}
#[test]
fn test_action_assertions_empty() {
let empty: Vec<TestAction> = vec![];
let non_empty = vec![TestAction::Foo];
empty.assert_empty();
non_empty.assert_not_empty();
}
#[test]
fn test_action_assertions_count() {
let actions = vec![TestAction::Foo, TestAction::Bar(1), TestAction::Bar(2)];
actions.assert_count(3);
}
#[test]
fn test_action_assertions_matches() {
let actions = vec![TestAction::Foo, TestAction::Bar(42), TestAction::Bar(99)];
actions.assert_first_matches(|a| matches!(a, TestAction::Foo));
actions.assert_any_matches(|a| matches!(a, TestAction::Bar(x) if *x > 50));
actions.assert_all_match(|a| matches!(a, TestAction::Foo | TestAction::Bar(_)));
actions.assert_none_match(|a| matches!(a, TestAction::Bar(0)));
}
#[test]
fn test_keys_multiple() {
let k = keys("a b c");
assert_eq!(k.len(), 3);
assert_eq!(k[0].code, KeyCode::Char('a'));
assert_eq!(k[1].code, KeyCode::Char('b'));
assert_eq!(k[2].code, KeyCode::Char('c'));
}
#[test]
fn test_keys_with_modifiers() {
let k = keys("ctrl+c esc enter");
assert_eq!(k.len(), 3);
assert_eq!(k[0].code, KeyCode::Char('c'));
assert!(k[0].modifiers.contains(KeyModifiers::CONTROL));
assert_eq!(k[1].code, KeyCode::Esc);
assert_eq!(k[2].code, KeyCode::Enter);
}
#[test]
fn test_render_harness_new() {
let harness = RenderHarness::new(80, 24);
assert_eq!(harness.size(), (80, 24));
}
#[test]
fn test_render_harness_render_plain() {
let mut harness = RenderHarness::new(10, 2);
let output = harness.render_to_string_plain(|frame| {
use ratatui::widgets::Paragraph;
let p = Paragraph::new("Hello");
frame.render_widget(p, frame.area());
});
assert!(output.starts_with("Hello"));
}
#[test]
fn test_render_harness_resize() {
let mut harness = RenderHarness::new(80, 24);
assert_eq!(harness.size(), (80, 24));
harness.resize(100, 30);
assert_eq!(harness.size(), (100, 30));
}
#[test]
fn test_complete_action() {
let mut harness = TestHarness::<(), TestAction>::new(());
harness.complete_action(TestAction::Foo);
harness.complete_actions([TestAction::Bar(1), TestAction::Bar(2)]);
let actions = harness.drain_emitted();
assert_eq!(actions.len(), 3);
actions.assert_first(TestAction::Foo);
}
#[derive(Default, Debug, PartialEq)]
struct TestState {
count: i32,
name: String,
}
#[test]
fn test_assert_state_macro() {
let harness = TestHarness::<TestState, TestAction>::new(TestState {
count: 42,
name: "test".to_string(),
});
assert_state!(harness, count, 42);
assert_state!(harness, name, "test".to_string());
}
}