1use std::fmt::Debug;
34
35use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
36use tokio::sync::mpsc;
37
38use crate::event::{ComponentId, Event, EventContext, EventKind};
39use crate::keybindings::parse_key_string;
40use crate::{Action, ActionCategory};
41
42pub trait ActionAssertions<A> {
62 fn assert_empty(&self);
67
68 fn assert_not_empty(&self);
73
74 fn assert_count(&self, n: usize);
79
80 fn assert_first_matches<F: Fn(&A) -> bool>(&self, f: F);
85
86 fn assert_any_matches<F: Fn(&A) -> bool>(&self, f: F);
91
92 fn assert_all_match<F: Fn(&A) -> bool>(&self, f: F);
97
98 fn assert_none_match<F: Fn(&A) -> bool>(&self, f: F);
103}
104
105pub trait ActionAssertionsEq<A> {
120 fn assert_first(&self, expected: A);
125
126 fn assert_last(&self, expected: A);
131
132 fn assert_contains(&self, expected: A);
137
138 fn assert_not_contains(&self, expected: A);
143}
144
145impl<A: Debug> ActionAssertions<A> for Vec<A> {
147 fn assert_empty(&self) {
148 assert!(
149 self.is_empty(),
150 "Expected no actions to be emitted, but got: {:?}",
151 self
152 );
153 }
154
155 fn assert_not_empty(&self) {
156 assert!(
157 !self.is_empty(),
158 "Expected actions to be emitted, but got none"
159 );
160 }
161
162 fn assert_count(&self, n: usize) {
163 assert_eq!(
164 self.len(),
165 n,
166 "Expected {} action(s), got {}: {:?}",
167 n,
168 self.len(),
169 self
170 );
171 }
172
173 fn assert_first_matches<F: Fn(&A) -> bool>(&self, f: F) {
174 assert!(
175 !self.is_empty(),
176 "Expected first action to match predicate, but no actions were emitted"
177 );
178 assert!(
179 f(&self[0]),
180 "Expected first action to match predicate, got: {:?}",
181 self[0]
182 );
183 }
184
185 fn assert_any_matches<F: Fn(&A) -> bool>(&self, f: F) {
186 assert!(
187 self.iter().any(&f),
188 "Expected any action to match predicate, but none did: {:?}",
189 self
190 );
191 }
192
193 fn assert_all_match<F: Fn(&A) -> bool>(&self, f: F) {
194 for (i, action) in self.iter().enumerate() {
195 assert!(
196 f(action),
197 "Expected all actions to match predicate, but action at index {} didn't: {:?}",
198 i,
199 action
200 );
201 }
202 }
203
204 fn assert_none_match<F: Fn(&A) -> bool>(&self, f: F) {
205 for (i, action) in self.iter().enumerate() {
206 assert!(
207 !f(action),
208 "Expected no action to match predicate, but action at index {} matched: {:?}",
209 i,
210 action
211 );
212 }
213 }
214}
215
216impl<A: PartialEq + Debug> ActionAssertionsEq<A> for Vec<A> {
218 fn assert_first(&self, expected: A) {
219 assert!(
220 !self.is_empty(),
221 "Expected first action to be {:?}, but no actions were emitted",
222 expected
223 );
224 assert_eq!(
225 &self[0], &expected,
226 "Expected first action to be {:?}, got {:?}",
227 expected, self[0]
228 );
229 }
230
231 fn assert_last(&self, expected: A) {
232 assert!(
233 !self.is_empty(),
234 "Expected last action to be {:?}, but no actions were emitted",
235 expected
236 );
237 let last = self.last().unwrap();
238 assert_eq!(
239 last, &expected,
240 "Expected last action to be {:?}, got {:?}",
241 expected, last
242 );
243 }
244
245 fn assert_contains(&self, expected: A) {
246 assert!(
247 self.iter().any(|a| a == &expected),
248 "Expected actions to contain {:?}, but got: {:?}",
249 expected,
250 self
251 );
252 }
253
254 fn assert_not_contains(&self, expected: A) {
255 assert!(
256 !self.iter().any(|a| a == &expected),
257 "Expected actions NOT to contain {:?}, but it was found in: {:?}",
258 expected,
259 self
260 );
261 }
262}
263
264impl<A: Debug> ActionAssertions<A> for [A] {
266 fn assert_empty(&self) {
267 assert!(
268 self.is_empty(),
269 "Expected no actions to be emitted, but got: {:?}",
270 self
271 );
272 }
273
274 fn assert_not_empty(&self) {
275 assert!(
276 !self.is_empty(),
277 "Expected actions to be emitted, but got none"
278 );
279 }
280
281 fn assert_count(&self, n: usize) {
282 assert_eq!(
283 self.len(),
284 n,
285 "Expected {} action(s), got {}: {:?}",
286 n,
287 self.len(),
288 self
289 );
290 }
291
292 fn assert_first_matches<F: Fn(&A) -> bool>(&self, f: F) {
293 assert!(
294 !self.is_empty(),
295 "Expected first action to match predicate, but no actions were emitted"
296 );
297 assert!(
298 f(&self[0]),
299 "Expected first action to match predicate, got: {:?}",
300 self[0]
301 );
302 }
303
304 fn assert_any_matches<F: Fn(&A) -> bool>(&self, f: F) {
305 assert!(
306 self.iter().any(&f),
307 "Expected any action to match predicate, but none did: {:?}",
308 self
309 );
310 }
311
312 fn assert_all_match<F: Fn(&A) -> bool>(&self, f: F) {
313 for (i, action) in self.iter().enumerate() {
314 assert!(
315 f(action),
316 "Expected all actions to match predicate, but action at index {} didn't: {:?}",
317 i,
318 action
319 );
320 }
321 }
322
323 fn assert_none_match<F: Fn(&A) -> bool>(&self, f: F) {
324 for (i, action) in self.iter().enumerate() {
325 assert!(
326 !f(action),
327 "Expected no action to match predicate, but action at index {} matched: {:?}",
328 i,
329 action
330 );
331 }
332 }
333}
334
335impl<A: PartialEq + Debug> ActionAssertionsEq<A> for [A] {
337 fn assert_first(&self, expected: A) {
338 assert!(
339 !self.is_empty(),
340 "Expected first action to be {:?}, but no actions were emitted",
341 expected
342 );
343 assert_eq!(
344 &self[0], &expected,
345 "Expected first action to be {:?}, got {:?}",
346 expected, self[0]
347 );
348 }
349
350 fn assert_last(&self, expected: A) {
351 assert!(
352 !self.is_empty(),
353 "Expected last action to be {:?}, but no actions were emitted",
354 expected
355 );
356 let last = self.last().unwrap();
357 assert_eq!(
358 last, &expected,
359 "Expected last action to be {:?}, got {:?}",
360 expected, last
361 );
362 }
363
364 fn assert_contains(&self, expected: A) {
365 assert!(
366 self.iter().any(|a| a == &expected),
367 "Expected actions to contain {:?}, but got: {:?}",
368 expected,
369 self
370 );
371 }
372
373 fn assert_not_contains(&self, expected: A) {
374 assert!(
375 !self.iter().any(|a| a == &expected),
376 "Expected actions NOT to contain {:?}, but it was found in: {:?}",
377 expected,
378 self
379 );
380 }
381}
382
383pub fn key(s: &str) -> KeyEvent {
413 parse_key_string(s).unwrap_or_else(|| panic!("Invalid key string: {:?}", s))
414}
415
416pub fn char_key(c: char) -> KeyEvent {
428 KeyEvent {
429 code: KeyCode::Char(c),
430 modifiers: KeyModifiers::empty(),
431 kind: crossterm::event::KeyEventKind::Press,
432 state: crossterm::event::KeyEventState::empty(),
433 }
434}
435
436pub fn ctrl_key(c: char) -> KeyEvent {
438 KeyEvent {
439 code: KeyCode::Char(c),
440 modifiers: KeyModifiers::CONTROL,
441 kind: crossterm::event::KeyEventKind::Press,
442 state: crossterm::event::KeyEventState::empty(),
443 }
444}
445
446pub fn alt_key(c: char) -> KeyEvent {
448 KeyEvent {
449 code: KeyCode::Char(c),
450 modifiers: KeyModifiers::ALT,
451 kind: crossterm::event::KeyEventKind::Press,
452 state: crossterm::event::KeyEventState::empty(),
453 }
454}
455
456pub fn key_event<C: ComponentId>(s: &str) -> Event<C> {
469 Event {
470 kind: EventKind::Key(key(s)),
471 context: EventContext::default(),
472 }
473}
474
475pub fn into_event<C: ComponentId>(key_event: KeyEvent) -> Event<C> {
486 Event {
487 kind: EventKind::Key(key_event),
488 context: EventContext::default(),
489 }
490}
491
492pub fn key_events<C: ComponentId>(keys: &str) -> Vec<Event<C>> {
518 keys.split_whitespace().map(|k| key_event::<C>(k)).collect()
519}
520
521pub fn keys(key_str: &str) -> Vec<KeyEvent> {
538 key_str.split_whitespace().map(key).collect()
539}
540
541pub struct TestHarness<S, A: Action> {
572 pub state: S,
574 tx: mpsc::UnboundedSender<A>,
576 rx: mpsc::UnboundedReceiver<A>,
578}
579
580impl<S, A: Action> TestHarness<S, A> {
581 pub fn new(state: S) -> Self {
583 let (tx, rx) = mpsc::unbounded_channel();
584 Self { state, tx, rx }
585 }
586
587 pub fn sender(&self) -> mpsc::UnboundedSender<A> {
589 self.tx.clone()
590 }
591
592 pub fn emit(&self, action: A) {
594 let _ = self.tx.send(action);
595 }
596
597 pub fn drain_emitted(&mut self) -> Vec<A> {
599 let mut actions = Vec::new();
600 while let Ok(action) = self.rx.try_recv() {
601 actions.push(action);
602 }
603 actions
604 }
605
606 pub fn has_emitted(&mut self) -> bool {
608 !self.drain_emitted().is_empty()
609 }
610
611 pub fn complete_action(&self, action: A) {
624 self.emit(action);
625 }
626
627 pub fn complete_actions(&self, actions: impl IntoIterator<Item = A>) {
638 for action in actions {
639 self.emit(action);
640 }
641 }
642
643 pub fn send_keys<C, H, I>(&mut self, keys: &str, mut handler: H) -> Vec<A>
663 where
664 C: ComponentId,
665 I: IntoIterator<Item = A>,
666 H: FnMut(&mut S, Event<C>) -> I,
667 {
668 let events = key_events::<C>(keys);
669 let mut all_actions = Vec::new();
670 for event in events {
671 let actions = handler(&mut self.state, event);
672 all_actions.extend(actions);
673 }
674 all_actions
675 }
676
677 pub fn send_keys_emit<C, H, I>(&mut self, keys: &str, mut handler: H)
693 where
694 C: ComponentId,
695 I: IntoIterator<Item = A>,
696 H: FnMut(&mut S, Event<C>) -> I,
697 {
698 let events = key_events::<C>(keys);
699 for event in events {
700 let actions = handler(&mut self.state, event);
701 for action in actions {
702 self.emit(action);
703 }
704 }
705 }
706}
707
708impl<S: Default, A: Action> Default for TestHarness<S, A> {
709 fn default() -> Self {
710 Self::new(S::default())
711 }
712}
713
714impl<S, A: ActionCategory> TestHarness<S, A> {
719 pub fn drain_category(&mut self, category: &str) -> Vec<A> {
744 let all = self.drain_emitted();
745 let mut matching = Vec::new();
746 let mut non_matching = Vec::new();
747
748 for action in all {
749 if action.category() == Some(category) {
750 matching.push(action);
751 } else {
752 non_matching.push(action);
753 }
754 }
755
756 for action in non_matching {
758 let _ = self.tx.send(action);
759 }
760
761 matching
762 }
763
764 pub fn has_category(&mut self, category: &str) -> bool {
768 !self.drain_category(category).is_empty()
769 }
770}
771
772#[macro_export]
784macro_rules! assert_emitted {
785 ($actions:expr, $pattern:pat $(if $guard:expr)?) => {
786 assert!(
787 $actions.iter().any(|a| matches!(a, $pattern $(if $guard)?)),
788 "Expected action matching `{}` to be emitted, but got: {:?}",
789 stringify!($pattern),
790 $actions
791 );
792 };
793}
794
795#[macro_export]
806macro_rules! assert_not_emitted {
807 ($actions:expr, $pattern:pat $(if $guard:expr)?) => {
808 assert!(
809 !$actions.iter().any(|a| matches!(a, $pattern $(if $guard)?)),
810 "Expected action matching `{}` NOT to be emitted, but it was: {:?}",
811 stringify!($pattern),
812 $actions
813 );
814 };
815}
816
817#[macro_export]
830macro_rules! find_emitted {
831 ($actions:expr, $pattern:pat $(if $guard:expr)?) => {
832 $actions.iter().find(|a| matches!(a, $pattern $(if $guard)?))
833 };
834}
835
836#[macro_export]
847macro_rules! count_emitted {
848 ($actions:expr, $pattern:pat $(if $guard:expr)?) => {
849 $actions.iter().filter(|a| matches!(a, $pattern $(if $guard)?)).count()
850 };
851}
852
853#[macro_export]
867macro_rules! assert_category_emitted {
868 ($actions:expr, $category:expr) => {
869 assert!(
870 $actions.iter().any(|a| {
871 use $crate::ActionCategory;
872 a.category() == Some($category)
873 }),
874 "Expected action with category `{}` to be emitted, but got: {:?}",
875 $category,
876 $actions
877 );
878 };
879}
880
881#[macro_export]
894macro_rules! assert_category_not_emitted {
895 ($actions:expr, $category:expr) => {
896 assert!(
897 !$actions.iter().any(|a| {
898 use $crate::ActionCategory;
899 a.category() == Some($category)
900 }),
901 "Expected NO action with category `{}` to be emitted, but found: {:?}",
902 $category,
903 $actions
904 .iter()
905 .filter(|a| {
906 use $crate::ActionCategory;
907 a.category() == Some($category)
908 })
909 .collect::<Vec<_>>()
910 );
911 };
912}
913
914#[macro_export]
927macro_rules! count_category {
928 ($actions:expr, $category:expr) => {{
929 use $crate::ActionCategory;
930 $actions
931 .iter()
932 .filter(|a| a.category() == Some($category))
933 .count()
934 }};
935}
936
937#[macro_export]
953macro_rules! assert_state {
954 ($harness:expr, $($field:tt).+, $expected:expr) => {
955 assert_eq!(
956 $harness.state.$($field).+,
957 $expected,
958 "Expected state.{} = {:?}, got {:?}",
959 stringify!($($field).+),
960 $expected,
961 $harness.state.$($field).+
962 );
963 };
964}
965
966#[macro_export]
976macro_rules! assert_state_matches {
977 ($harness:expr, $($field:tt).+, $pattern:pat $(if $guard:expr)?) => {
978 assert!(
979 matches!($harness.state.$($field).+, $pattern $(if $guard)?),
980 "Expected state.{} to match `{}`, got {:?}",
981 stringify!($($field).+),
982 stringify!($pattern),
983 $harness.state.$($field).+
984 );
985 };
986}
987
988use ratatui::backend::{Backend, TestBackend};
993use ratatui::buffer::Buffer;
994use ratatui::Terminal;
995
996pub struct RenderHarness {
1017 terminal: Terminal<TestBackend>,
1018}
1019
1020impl RenderHarness {
1021 pub fn new(width: u16, height: u16) -> Self {
1023 let backend = TestBackend::new(width, height);
1024 let terminal = Terminal::new(backend).expect("Failed to create test terminal");
1025 Self { terminal }
1026 }
1027
1028 pub fn render<F>(&mut self, render_fn: F) -> &Buffer
1030 where
1031 F: FnOnce(&mut ratatui::Frame),
1032 {
1033 self.terminal
1034 .draw(render_fn)
1035 .expect("Failed to draw to test terminal");
1036 self.terminal.backend().buffer()
1037 }
1038
1039 pub fn render_to_string<F>(&mut self, render_fn: F) -> String
1043 where
1044 F: FnOnce(&mut ratatui::Frame),
1045 {
1046 let buffer = self.render(render_fn);
1047 buffer_to_string(buffer)
1048 }
1049
1050 pub fn render_to_string_plain<F>(&mut self, render_fn: F) -> String
1054 where
1055 F: FnOnce(&mut ratatui::Frame),
1056 {
1057 let buffer = self.render(render_fn);
1058 buffer_to_string_plain(buffer)
1059 }
1060
1061 pub fn size(&self) -> (u16, u16) {
1063 let area = self.terminal.backend().size().unwrap_or_default();
1064 (area.width, area.height)
1065 }
1066
1067 pub fn resize(&mut self, width: u16, height: u16) {
1069 self.terminal.backend_mut().resize(width, height);
1070 }
1071}
1072
1073pub fn buffer_to_string(buffer: &Buffer) -> String {
1078 use ratatui::style::{Color, Modifier};
1079 use std::fmt::Write;
1080
1081 let area = buffer.area();
1082 let mut result = String::new();
1083
1084 for y in area.top()..area.bottom() {
1085 for x in area.left()..area.right() {
1086 let cell = &buffer[(x, y)];
1087
1088 let _ = write!(result, "\x1b[0m");
1090
1091 match cell.fg {
1093 Color::Reset => {}
1094 Color::Black => result.push_str("\x1b[30m"),
1095 Color::Red => result.push_str("\x1b[31m"),
1096 Color::Green => result.push_str("\x1b[32m"),
1097 Color::Yellow => result.push_str("\x1b[33m"),
1098 Color::Blue => result.push_str("\x1b[34m"),
1099 Color::Magenta => result.push_str("\x1b[35m"),
1100 Color::Cyan => result.push_str("\x1b[36m"),
1101 Color::Gray => result.push_str("\x1b[37m"),
1102 Color::DarkGray => result.push_str("\x1b[90m"),
1103 Color::LightRed => result.push_str("\x1b[91m"),
1104 Color::LightGreen => result.push_str("\x1b[92m"),
1105 Color::LightYellow => result.push_str("\x1b[93m"),
1106 Color::LightBlue => result.push_str("\x1b[94m"),
1107 Color::LightMagenta => result.push_str("\x1b[95m"),
1108 Color::LightCyan => result.push_str("\x1b[96m"),
1109 Color::White => result.push_str("\x1b[97m"),
1110 Color::Rgb(r, g, b) => {
1111 let _ = write!(result, "\x1b[38;2;{};{};{}m", r, g, b);
1112 }
1113 Color::Indexed(i) => {
1114 let _ = write!(result, "\x1b[38;5;{}m", i);
1115 }
1116 }
1117
1118 match cell.bg {
1120 Color::Reset => {}
1121 Color::Black => result.push_str("\x1b[40m"),
1122 Color::Red => result.push_str("\x1b[41m"),
1123 Color::Green => result.push_str("\x1b[42m"),
1124 Color::Yellow => result.push_str("\x1b[43m"),
1125 Color::Blue => result.push_str("\x1b[44m"),
1126 Color::Magenta => result.push_str("\x1b[45m"),
1127 Color::Cyan => result.push_str("\x1b[46m"),
1128 Color::Gray => result.push_str("\x1b[47m"),
1129 Color::DarkGray => result.push_str("\x1b[100m"),
1130 Color::LightRed => result.push_str("\x1b[101m"),
1131 Color::LightGreen => result.push_str("\x1b[102m"),
1132 Color::LightYellow => result.push_str("\x1b[103m"),
1133 Color::LightBlue => result.push_str("\x1b[104m"),
1134 Color::LightMagenta => result.push_str("\x1b[105m"),
1135 Color::LightCyan => result.push_str("\x1b[106m"),
1136 Color::White => result.push_str("\x1b[107m"),
1137 Color::Rgb(r, g, b) => {
1138 let _ = write!(result, "\x1b[48;2;{};{};{}m", r, g, b);
1139 }
1140 Color::Indexed(i) => {
1141 let _ = write!(result, "\x1b[48;5;{}m", i);
1142 }
1143 }
1144
1145 if cell.modifier.contains(Modifier::BOLD) {
1147 result.push_str("\x1b[1m");
1148 }
1149 if cell.modifier.contains(Modifier::DIM) {
1150 result.push_str("\x1b[2m");
1151 }
1152 if cell.modifier.contains(Modifier::ITALIC) {
1153 result.push_str("\x1b[3m");
1154 }
1155 if cell.modifier.contains(Modifier::UNDERLINED) {
1156 result.push_str("\x1b[4m");
1157 }
1158 if cell.modifier.contains(Modifier::REVERSED) {
1159 result.push_str("\x1b[7m");
1160 }
1161 if cell.modifier.contains(Modifier::CROSSED_OUT) {
1162 result.push_str("\x1b[9m");
1163 }
1164
1165 result.push_str(cell.symbol());
1166 }
1167 result.push_str("\x1b[0m\n");
1168 }
1169
1170 result
1171}
1172
1173pub fn buffer_to_string_plain(buffer: &Buffer) -> String {
1177 let area = buffer.area();
1178 let mut result = String::new();
1179
1180 for y in area.top()..area.bottom() {
1181 for x in area.left()..area.right() {
1182 let cell = &buffer[(x, y)];
1183 result.push_str(cell.symbol());
1184 }
1185 result.push('\n');
1186 }
1187
1188 result
1189}
1190
1191pub fn buffer_rect_to_string_plain(buffer: &Buffer, rect: ratatui::layout::Rect) -> String {
1195 let mut result = String::new();
1196
1197 for y in rect.top()..rect.bottom() {
1198 for x in rect.left()..rect.right() {
1199 if x < buffer.area().right() && y < buffer.area().bottom() {
1200 let cell = &buffer[(x, y)];
1201 result.push_str(cell.symbol());
1202 }
1203 }
1204 result.push('\n');
1205 }
1206
1207 result
1208}
1209
1210pub struct StoreTestHarness<S, A: Action, E = crate::NoEffect> {
1244 store: crate::Store<S, A, E>,
1245 tx: mpsc::UnboundedSender<A>,
1246 rx: mpsc::UnboundedReceiver<A>,
1247 effects: Vec<E>,
1248 render: Option<RenderHarness>,
1249 default_size: (u16, u16),
1250}
1251
1252impl<S, A: Action, E> StoreTestHarness<S, A, E> {
1253 pub fn new(state: S, reducer: crate::Reducer<S, A, E>) -> Self {
1255 let (tx, rx) = mpsc::unbounded_channel();
1256 Self {
1257 store: crate::Store::new(state, reducer),
1258 tx,
1259 rx,
1260 effects: Vec::new(),
1261 render: None,
1262 default_size: (80, 24),
1263 }
1264 }
1265
1266 pub fn with_size(mut self, width: u16, height: u16) -> Self {
1268 self.default_size = (width, height);
1269 self
1270 }
1271
1272 pub fn dispatch(&mut self, action: A) -> crate::ReducerResult<E> {
1278 self.store.dispatch(action)
1279 }
1280
1281 pub fn dispatch_collect(&mut self, action: A) -> bool {
1286 let result = self.store.dispatch(action);
1287 self.effects.extend(result.effects);
1288 result.changed
1289 }
1290
1291 pub fn dispatch_all(&mut self, actions: impl IntoIterator<Item = A>) -> Vec<bool> {
1295 actions
1296 .into_iter()
1297 .map(|a| self.dispatch_collect(a))
1298 .collect()
1299 }
1300
1301 pub fn state(&self) -> &S {
1303 self.store.state()
1304 }
1305
1306 pub fn state_mut(&mut self) -> &mut S {
1308 self.store.state_mut()
1309 }
1310
1311 pub fn drain_effects(&mut self) -> Vec<E> {
1315 std::mem::take(&mut self.effects)
1316 }
1317
1318 pub fn has_effects(&self) -> bool {
1320 !self.effects.is_empty()
1321 }
1322
1323 pub fn effect_count(&self) -> usize {
1325 self.effects.len()
1326 }
1327
1328 pub fn sender(&self) -> mpsc::UnboundedSender<A> {
1332 self.tx.clone()
1333 }
1334
1335 pub fn emit(&self, action: A) {
1337 let _ = self.tx.send(action);
1338 }
1339
1340 pub fn complete_action(&self, action: A) {
1342 self.emit(action);
1343 }
1344
1345 pub fn complete_actions(&self, actions: impl IntoIterator<Item = A>) {
1347 for action in actions {
1348 self.emit(action);
1349 }
1350 }
1351
1352 pub fn drain_emitted(&mut self) -> Vec<A> {
1354 let mut actions = Vec::new();
1355 while let Ok(action) = self.rx.try_recv() {
1356 actions.push(action);
1357 }
1358 actions
1359 }
1360
1361 pub fn has_emitted(&mut self) -> bool {
1363 !self.drain_emitted().is_empty()
1364 }
1365
1366 pub fn process_emitted(&mut self) -> (usize, usize) {
1371 let actions = self.drain_emitted();
1372 let total = actions.len();
1373 let changed = actions
1374 .into_iter()
1375 .filter(|a| self.dispatch_collect(a.clone()))
1376 .count();
1377 (changed, total)
1378 }
1379
1380 pub fn send_keys<C, H, I>(&mut self, keys: &str, mut handler: H) -> Vec<A>
1387 where
1388 C: ComponentId,
1389 I: IntoIterator<Item = A>,
1390 H: FnMut(&mut S, Event<C>) -> I,
1391 {
1392 let events = key_events::<C>(keys);
1393 let mut all_actions = Vec::new();
1394 for event in events {
1395 let actions = handler(self.store.state_mut(), event);
1396 all_actions.extend(actions);
1397 }
1398 all_actions
1399 }
1400
1401 pub fn send_keys_dispatch<C, H, I>(&mut self, keys: &str, mut handler: H) -> Vec<A>
1405 where
1406 C: ComponentId,
1407 I: IntoIterator<Item = A>,
1408 H: FnMut(&mut S, Event<C>) -> I,
1409 {
1410 let events = key_events::<C>(keys);
1411 let mut all_actions = Vec::new();
1412 for event in events {
1413 let actions: Vec<A> = handler(self.store.state_mut(), event).into_iter().collect();
1414 for action in actions {
1415 self.dispatch_collect(action.clone());
1416 all_actions.push(action);
1417 }
1418 }
1419 all_actions
1420 }
1421
1422 pub fn assert_state<F>(&self, predicate: F)
1430 where
1431 F: FnOnce(&S) -> bool,
1432 {
1433 assert!(predicate(self.state()), "State assertion failed");
1434 }
1435
1436 pub fn assert_state_msg<F>(&self, predicate: F, msg: &str)
1438 where
1439 F: FnOnce(&S) -> bool,
1440 {
1441 assert!(predicate(self.state()), "{}", msg);
1442 }
1443
1444 fn ensure_render(&mut self, width: u16, height: u16) {
1447 if self.render.is_none() || self.render.as_ref().map(|r| r.size()) != Some((width, height))
1448 {
1449 self.render = Some(RenderHarness::new(width, height));
1450 }
1451 }
1452
1453 pub fn render<F>(&mut self, width: u16, height: u16, render_fn: F) -> String
1455 where
1456 F: FnOnce(&mut ratatui::Frame, ratatui::layout::Rect, &S),
1457 {
1458 self.ensure_render(width, height);
1459 let store = &self.store;
1460 let render = self.render.as_mut().unwrap();
1461 render.render_to_string(|frame| {
1462 let area = frame.area();
1463 render_fn(frame, area, store.state());
1464 })
1465 }
1466
1467 pub fn render_plain<F>(&mut self, width: u16, height: u16, render_fn: F) -> String
1469 where
1470 F: FnOnce(&mut ratatui::Frame, ratatui::layout::Rect, &S),
1471 {
1472 self.ensure_render(width, height);
1473 let store = &self.store;
1474 let render = self.render.as_mut().unwrap();
1475 render.render_to_string_plain(|frame| {
1476 let area = frame.area();
1477 render_fn(frame, area, store.state());
1478 })
1479 }
1480
1481 pub fn render_default<F>(&mut self, render_fn: F) -> String
1483 where
1484 F: FnOnce(&mut ratatui::Frame, ratatui::layout::Rect, &S),
1485 {
1486 let (w, h) = self.default_size;
1487 self.render(w, h, render_fn)
1488 }
1489
1490 pub fn render_default_plain<F>(&mut self, render_fn: F) -> String
1492 where
1493 F: FnOnce(&mut ratatui::Frame, ratatui::layout::Rect, &S),
1494 {
1495 let (w, h) = self.default_size;
1496 self.render_plain(w, h, render_fn)
1497 }
1498}
1499
1500impl<S: Default, A: Action> Default for StoreTestHarness<S, A> {
1501 fn default() -> Self {
1502 Self::new(S::default(), |_, _| crate::ReducerResult::unchanged())
1503 }
1504}
1505
1506pub trait EffectAssertions<E> {
1525 fn effects_empty(&self);
1527 fn effects_not_empty(&self);
1529 fn effects_count(&self, n: usize);
1531 fn effects_any_matches<F: Fn(&E) -> bool>(&self, f: F);
1533 fn effects_first_matches<F: Fn(&E) -> bool>(&self, f: F);
1535 fn effects_all_match<F: Fn(&E) -> bool>(&self, f: F);
1537 fn effects_none_match<F: Fn(&E) -> bool>(&self, f: F);
1539}
1540
1541impl<E: std::fmt::Debug> EffectAssertions<E> for Vec<E> {
1542 fn effects_empty(&self) {
1543 assert!(self.is_empty(), "Expected no effects, got {:?}", self);
1544 }
1545
1546 fn effects_not_empty(&self) {
1547 assert!(!self.is_empty(), "Expected effects, got none");
1548 }
1549
1550 fn effects_count(&self, n: usize) {
1551 assert_eq!(
1552 self.len(),
1553 n,
1554 "Expected {} effects, got {}: {:?}",
1555 n,
1556 self.len(),
1557 self
1558 );
1559 }
1560
1561 fn effects_any_matches<F: Fn(&E) -> bool>(&self, f: F) {
1562 assert!(
1563 self.iter().any(&f),
1564 "No effect matched predicate in {:?}",
1565 self
1566 );
1567 }
1568
1569 fn effects_first_matches<F: Fn(&E) -> bool>(&self, f: F) {
1570 let first = self.first().expect("Expected at least one effect");
1571 assert!(f(first), "First effect {:?} did not match predicate", first);
1572 }
1573
1574 fn effects_all_match<F: Fn(&E) -> bool>(&self, f: F) {
1575 for (i, e) in self.iter().enumerate() {
1576 assert!(f(e), "Effect at index {} did not match: {:?}", i, e);
1577 }
1578 }
1579
1580 fn effects_none_match<F: Fn(&E) -> bool>(&self, f: F) {
1581 for (i, e) in self.iter().enumerate() {
1582 assert!(!f(e), "Effect at index {} unexpectedly matched: {:?}", i, e);
1583 }
1584 }
1585}
1586
1587pub trait EffectAssertionsEq<E> {
1592 fn effects_contains(&self, expected: E);
1594 fn effects_first_eq(&self, expected: E);
1596 fn effects_last_eq(&self, expected: E);
1598}
1599
1600impl<E: PartialEq + std::fmt::Debug> EffectAssertionsEq<E> for Vec<E> {
1601 fn effects_contains(&self, expected: E) {
1602 assert!(
1603 self.contains(&expected),
1604 "Expected to contain {:?}, got {:?}",
1605 expected,
1606 self
1607 );
1608 }
1609
1610 fn effects_first_eq(&self, expected: E) {
1611 let first = self.first().expect("Expected at least one effect");
1612 assert_eq!(first, &expected, "First effect mismatch");
1613 }
1614
1615 fn effects_last_eq(&self, expected: E) {
1616 let last = self.last().expect("Expected at least one effect");
1617 assert_eq!(last, &expected, "Last effect mismatch");
1618 }
1619}
1620
1621#[cfg(feature = "testing-time")]
1651pub fn pause_time() {
1652 tokio::time::pause();
1653}
1654
1655#[cfg(feature = "testing-time")]
1657pub fn resume_time() {
1658 tokio::time::resume();
1659}
1660
1661#[cfg(feature = "testing-time")]
1665pub async fn advance_time(duration: std::time::Duration) {
1666 tokio::time::advance(duration).await;
1667}
1668
1669#[cfg(test)]
1670mod tests {
1671 use super::*;
1672
1673 #[test]
1674 fn test_key_simple() {
1675 let k = key("q");
1676 assert_eq!(k.code, KeyCode::Char('q'));
1677 assert_eq!(k.modifiers, KeyModifiers::empty());
1678 }
1679
1680 #[test]
1681 fn test_key_with_ctrl() {
1682 let k = key("ctrl+p");
1683 assert_eq!(k.code, KeyCode::Char('p'));
1684 assert!(k.modifiers.contains(KeyModifiers::CONTROL));
1685 }
1686
1687 #[test]
1688 fn test_key_special() {
1689 let k = key("esc");
1690 assert_eq!(k.code, KeyCode::Esc);
1691
1692 let k = key("enter");
1693 assert_eq!(k.code, KeyCode::Enter);
1694
1695 let k = key("shift+tab");
1696 assert_eq!(k.code, KeyCode::BackTab);
1697 }
1698
1699 #[test]
1700 fn test_char_key() {
1701 let k = char_key('x');
1702 assert_eq!(k.code, KeyCode::Char('x'));
1703 assert_eq!(k.modifiers, KeyModifiers::empty());
1704 }
1705
1706 #[test]
1707 fn test_ctrl_key() {
1708 let k = ctrl_key('c');
1709 assert_eq!(k.code, KeyCode::Char('c'));
1710 assert!(k.modifiers.contains(KeyModifiers::CONTROL));
1711 }
1712
1713 #[derive(Clone, Debug, PartialEq)]
1714 enum TestAction {
1715 Foo,
1716 Bar(i32),
1717 }
1718
1719 impl crate::Action for TestAction {
1720 fn name(&self) -> &'static str {
1721 match self {
1722 TestAction::Foo => "Foo",
1723 TestAction::Bar(_) => "Bar",
1724 }
1725 }
1726 }
1727
1728 #[test]
1729 fn test_harness_emit_and_drain() {
1730 let mut harness = TestHarness::<(), TestAction>::new(());
1731
1732 harness.emit(TestAction::Foo);
1733 harness.emit(TestAction::Bar(42));
1734
1735 let actions = harness.drain_emitted();
1736 assert_eq!(actions.len(), 2);
1737 assert_eq!(actions[0], TestAction::Foo);
1738 assert_eq!(actions[1], TestAction::Bar(42));
1739
1740 let actions = harness.drain_emitted();
1742 assert!(actions.is_empty());
1743 }
1744
1745 #[test]
1746 fn test_assert_macros() {
1747 let actions = vec![TestAction::Foo, TestAction::Bar(42)];
1748
1749 assert_emitted!(actions, TestAction::Foo);
1750 assert_emitted!(actions, TestAction::Bar(42));
1751 assert_emitted!(actions, TestAction::Bar(_));
1752
1753 assert_not_emitted!(actions, TestAction::Bar(99));
1754
1755 let found = find_emitted!(actions, TestAction::Bar(_));
1756 assert!(found.is_some());
1757
1758 let count = count_emitted!(actions, TestAction::Bar(_));
1759 assert_eq!(count, 1);
1760 }
1761
1762 #[test]
1764 fn test_action_assertions_first_last() {
1765 let actions = vec![TestAction::Foo, TestAction::Bar(42), TestAction::Bar(99)];
1766
1767 actions.assert_first(TestAction::Foo);
1768 actions.assert_last(TestAction::Bar(99));
1769 }
1770
1771 #[test]
1772 fn test_action_assertions_contains() {
1773 let actions = vec![TestAction::Foo, TestAction::Bar(42)];
1774
1775 actions.assert_contains(TestAction::Foo);
1776 actions.assert_contains(TestAction::Bar(42));
1777 actions.assert_not_contains(TestAction::Bar(99));
1778 }
1779
1780 #[test]
1781 fn test_action_assertions_empty() {
1782 let empty: Vec<TestAction> = vec![];
1783 let non_empty = vec![TestAction::Foo];
1784
1785 empty.assert_empty();
1786 non_empty.assert_not_empty();
1787 }
1788
1789 #[test]
1790 fn test_action_assertions_count() {
1791 let actions = vec![TestAction::Foo, TestAction::Bar(1), TestAction::Bar(2)];
1792 actions.assert_count(3);
1793 }
1794
1795 #[test]
1796 fn test_action_assertions_matches() {
1797 let actions = vec![TestAction::Foo, TestAction::Bar(42), TestAction::Bar(99)];
1798
1799 actions.assert_first_matches(|a| matches!(a, TestAction::Foo));
1800 actions.assert_any_matches(|a| matches!(a, TestAction::Bar(x) if *x > 50));
1801 actions.assert_all_match(|a| matches!(a, TestAction::Foo | TestAction::Bar(_)));
1802 actions.assert_none_match(|a| matches!(a, TestAction::Bar(0)));
1803 }
1804
1805 #[test]
1807 fn test_keys_multiple() {
1808 let k = keys("a b c");
1809 assert_eq!(k.len(), 3);
1810 assert_eq!(k[0].code, KeyCode::Char('a'));
1811 assert_eq!(k[1].code, KeyCode::Char('b'));
1812 assert_eq!(k[2].code, KeyCode::Char('c'));
1813 }
1814
1815 #[test]
1816 fn test_keys_with_modifiers() {
1817 let k = keys("ctrl+c esc enter");
1818 assert_eq!(k.len(), 3);
1819 assert_eq!(k[0].code, KeyCode::Char('c'));
1820 assert!(k[0].modifiers.contains(KeyModifiers::CONTROL));
1821 assert_eq!(k[1].code, KeyCode::Esc);
1822 assert_eq!(k[2].code, KeyCode::Enter);
1823 }
1824
1825 #[test]
1827 fn test_render_harness_new() {
1828 let harness = RenderHarness::new(80, 24);
1829 assert_eq!(harness.size(), (80, 24));
1830 }
1831
1832 #[test]
1833 fn test_render_harness_render_plain() {
1834 let mut harness = RenderHarness::new(10, 2);
1835 let output = harness.render_to_string_plain(|frame| {
1836 use ratatui::widgets::Paragraph;
1837 let p = Paragraph::new("Hello");
1838 frame.render_widget(p, frame.area());
1839 });
1840
1841 assert!(output.starts_with("Hello"));
1843 }
1844
1845 #[test]
1846 fn test_render_harness_resize() {
1847 let mut harness = RenderHarness::new(80, 24);
1848 assert_eq!(harness.size(), (80, 24));
1849
1850 harness.resize(100, 30);
1851 assert_eq!(harness.size(), (100, 30));
1852 }
1853
1854 #[test]
1856 fn test_complete_action() {
1857 let mut harness = TestHarness::<(), TestAction>::new(());
1858
1859 harness.complete_action(TestAction::Foo);
1860 harness.complete_actions([TestAction::Bar(1), TestAction::Bar(2)]);
1861
1862 let actions = harness.drain_emitted();
1863 assert_eq!(actions.len(), 3);
1864 actions.assert_first(TestAction::Foo);
1865 }
1866
1867 #[derive(Default, Debug, PartialEq)]
1869 struct TestState {
1870 count: i32,
1871 name: String,
1872 }
1873
1874 #[test]
1875 fn test_assert_state_macro() {
1876 let harness = TestHarness::<TestState, TestAction>::new(TestState {
1877 count: 42,
1878 name: "test".to_string(),
1879 });
1880
1881 assert_state!(harness, count, 42);
1882 assert_state!(harness, name, "test".to_string());
1883 }
1884}