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> {
1243 store: crate::Store<S, A>,
1244 tx: mpsc::UnboundedSender<A>,
1245 rx: mpsc::UnboundedReceiver<A>,
1246 render: Option<RenderHarness>,
1247 default_size: (u16, u16),
1248}
1249
1250impl<S, A: Action> StoreTestHarness<S, A> {
1251 pub fn new(state: S, reducer: crate::Reducer<S, A>) -> Self {
1253 let (tx, rx) = mpsc::unbounded_channel();
1254 Self {
1255 store: crate::Store::new(state, reducer),
1256 tx,
1257 rx,
1258 render: None,
1259 default_size: (80, 24),
1260 }
1261 }
1262
1263 pub fn with_size(mut self, width: u16, height: u16) -> Self {
1265 self.default_size = (width, height);
1266 self
1267 }
1268
1269 pub fn dispatch(&mut self, action: A) -> bool {
1275 self.store.dispatch(action)
1276 }
1277
1278 pub fn dispatch_all(&mut self, actions: impl IntoIterator<Item = A>) -> Vec<bool> {
1282 actions.into_iter().map(|a| self.dispatch(a)).collect()
1283 }
1284
1285 pub fn state(&self) -> &S {
1287 self.store.state()
1288 }
1289
1290 pub fn state_mut(&mut self) -> &mut S {
1292 self.store.state_mut()
1293 }
1294
1295 pub fn sender(&self) -> mpsc::UnboundedSender<A> {
1299 self.tx.clone()
1300 }
1301
1302 pub fn emit(&self, action: A) {
1304 let _ = self.tx.send(action);
1305 }
1306
1307 pub fn complete_action(&self, action: A) {
1309 self.emit(action);
1310 }
1311
1312 pub fn complete_actions(&self, actions: impl IntoIterator<Item = A>) {
1314 for action in actions {
1315 self.emit(action);
1316 }
1317 }
1318
1319 pub fn drain_emitted(&mut self) -> Vec<A> {
1321 let mut actions = Vec::new();
1322 while let Ok(action) = self.rx.try_recv() {
1323 actions.push(action);
1324 }
1325 actions
1326 }
1327
1328 pub fn has_emitted(&mut self) -> bool {
1330 !self.drain_emitted().is_empty()
1331 }
1332
1333 pub fn process_emitted(&mut self) -> (usize, usize) {
1338 let actions = self.drain_emitted();
1339 let total = actions.len();
1340 let changed = actions
1341 .into_iter()
1342 .filter(|a| self.dispatch(a.clone()))
1343 .count();
1344 (changed, total)
1345 }
1346
1347 pub fn send_keys<C, H, I>(&mut self, keys: &str, mut handler: H) -> Vec<A>
1354 where
1355 C: ComponentId,
1356 I: IntoIterator<Item = A>,
1357 H: FnMut(&mut S, Event<C>) -> I,
1358 {
1359 let events = key_events::<C>(keys);
1360 let mut all_actions = Vec::new();
1361 for event in events {
1362 let actions = handler(self.store.state_mut(), event);
1363 all_actions.extend(actions);
1364 }
1365 all_actions
1366 }
1367
1368 pub fn send_keys_dispatch<C, H, I>(&mut self, keys: &str, mut handler: H) -> Vec<A>
1372 where
1373 C: ComponentId,
1374 I: IntoIterator<Item = A>,
1375 H: FnMut(&mut S, Event<C>) -> I,
1376 {
1377 let events = key_events::<C>(keys);
1378 let mut all_actions = Vec::new();
1379 for event in events {
1380 let actions: Vec<A> = handler(self.store.state_mut(), event).into_iter().collect();
1381 for action in actions {
1382 self.dispatch(action.clone());
1383 all_actions.push(action);
1384 }
1385 }
1386 all_actions
1387 }
1388
1389 pub fn assert_state<F>(&self, predicate: F)
1397 where
1398 F: FnOnce(&S) -> bool,
1399 {
1400 assert!(predicate(self.state()), "State assertion failed");
1401 }
1402
1403 pub fn assert_state_msg<F>(&self, predicate: F, msg: &str)
1405 where
1406 F: FnOnce(&S) -> bool,
1407 {
1408 assert!(predicate(self.state()), "{}", msg);
1409 }
1410
1411 fn ensure_render(&mut self, width: u16, height: u16) {
1414 if self.render.is_none() || self.render.as_ref().map(|r| r.size()) != Some((width, height))
1415 {
1416 self.render = Some(RenderHarness::new(width, height));
1417 }
1418 }
1419
1420 pub fn render<F>(&mut self, width: u16, height: u16, render_fn: F) -> String
1422 where
1423 F: FnOnce(&mut ratatui::Frame, ratatui::layout::Rect, &S),
1424 {
1425 self.ensure_render(width, height);
1426 let store = &self.store;
1427 let render = self.render.as_mut().unwrap();
1428 render.render_to_string(|frame| {
1429 let area = frame.area();
1430 render_fn(frame, area, store.state());
1431 })
1432 }
1433
1434 pub fn render_plain<F>(&mut self, width: u16, height: u16, render_fn: F) -> String
1436 where
1437 F: FnOnce(&mut ratatui::Frame, ratatui::layout::Rect, &S),
1438 {
1439 self.ensure_render(width, height);
1440 let store = &self.store;
1441 let render = self.render.as_mut().unwrap();
1442 render.render_to_string_plain(|frame| {
1443 let area = frame.area();
1444 render_fn(frame, area, store.state());
1445 })
1446 }
1447
1448 pub fn render_default<F>(&mut self, render_fn: F) -> String
1450 where
1451 F: FnOnce(&mut ratatui::Frame, ratatui::layout::Rect, &S),
1452 {
1453 let (w, h) = self.default_size;
1454 self.render(w, h, render_fn)
1455 }
1456
1457 pub fn render_default_plain<F>(&mut self, render_fn: F) -> String
1459 where
1460 F: FnOnce(&mut ratatui::Frame, ratatui::layout::Rect, &S),
1461 {
1462 let (w, h) = self.default_size;
1463 self.render_plain(w, h, render_fn)
1464 }
1465}
1466
1467impl<S: Default, A: Action> Default for StoreTestHarness<S, A> {
1468 fn default() -> Self {
1469 Self::new(S::default(), |_, _| false)
1470 }
1471}
1472
1473pub struct EffectStoreTestHarness<S, A: Action, E> {
1504 store: crate::EffectStore<S, A, E>,
1505 tx: mpsc::UnboundedSender<A>,
1506 rx: mpsc::UnboundedReceiver<A>,
1507 effects: Vec<E>,
1508 render: Option<RenderHarness>,
1509 default_size: (u16, u16),
1510}
1511
1512impl<S, A: Action, E> EffectStoreTestHarness<S, A, E> {
1513 pub fn new(state: S, reducer: crate::EffectReducer<S, A, E>) -> Self {
1515 let (tx, rx) = mpsc::unbounded_channel();
1516 Self {
1517 store: crate::EffectStore::new(state, reducer),
1518 tx,
1519 rx,
1520 effects: Vec::new(),
1521 render: None,
1522 default_size: (80, 24),
1523 }
1524 }
1525
1526 pub fn with_size(mut self, width: u16, height: u16) -> Self {
1528 self.default_size = (width, height);
1529 self
1530 }
1531
1532 pub fn dispatch(&mut self, action: A) -> crate::DispatchResult<E> {
1538 self.store.dispatch(action)
1539 }
1540
1541 pub fn dispatch_collect(&mut self, action: A) -> bool {
1546 let result = self.store.dispatch(action);
1547 self.effects.extend(result.effects);
1548 result.changed
1549 }
1550
1551 pub fn dispatch_all(&mut self, actions: impl IntoIterator<Item = A>) -> Vec<bool> {
1555 actions
1556 .into_iter()
1557 .map(|a| self.dispatch_collect(a))
1558 .collect()
1559 }
1560
1561 pub fn state(&self) -> &S {
1563 self.store.state()
1564 }
1565
1566 pub fn state_mut(&mut self) -> &mut S {
1568 self.store.state_mut()
1569 }
1570
1571 pub fn drain_effects(&mut self) -> Vec<E> {
1575 std::mem::take(&mut self.effects)
1576 }
1577
1578 pub fn has_effects(&self) -> bool {
1580 !self.effects.is_empty()
1581 }
1582
1583 pub fn effect_count(&self) -> usize {
1585 self.effects.len()
1586 }
1587
1588 pub fn sender(&self) -> mpsc::UnboundedSender<A> {
1592 self.tx.clone()
1593 }
1594
1595 pub fn emit(&self, action: A) {
1597 let _ = self.tx.send(action);
1598 }
1599
1600 pub fn complete_action(&self, action: A) {
1602 self.emit(action);
1603 }
1604
1605 pub fn complete_actions(&self, actions: impl IntoIterator<Item = A>) {
1607 for action in actions {
1608 self.emit(action);
1609 }
1610 }
1611
1612 pub fn drain_emitted(&mut self) -> Vec<A> {
1614 let mut actions = Vec::new();
1615 while let Ok(action) = self.rx.try_recv() {
1616 actions.push(action);
1617 }
1618 actions
1619 }
1620
1621 pub fn has_emitted(&mut self) -> bool {
1623 !self.drain_emitted().is_empty()
1624 }
1625
1626 pub fn process_emitted(&mut self) -> (usize, usize) {
1631 let actions = self.drain_emitted();
1632 let total = actions.len();
1633 let changed = actions
1634 .into_iter()
1635 .filter(|a| self.dispatch_collect(a.clone()))
1636 .count();
1637 (changed, total)
1638 }
1639
1640 pub fn send_keys<C, H, I>(&mut self, keys: &str, mut handler: H) -> Vec<A>
1644 where
1645 C: ComponentId,
1646 I: IntoIterator<Item = A>,
1647 H: FnMut(&mut S, Event<C>) -> I,
1648 {
1649 let events = key_events::<C>(keys);
1650 let mut all_actions = Vec::new();
1651 for event in events {
1652 let actions = handler(self.store.state_mut(), event);
1653 all_actions.extend(actions);
1654 }
1655 all_actions
1656 }
1657
1658 pub fn send_keys_dispatch<C, H, I>(&mut self, keys: &str, mut handler: H) -> Vec<A>
1662 where
1663 C: ComponentId,
1664 I: IntoIterator<Item = A>,
1665 H: FnMut(&mut S, Event<C>) -> I,
1666 {
1667 let events = key_events::<C>(keys);
1668 let mut all_actions = Vec::new();
1669 for event in events {
1670 let actions: Vec<A> = handler(self.store.state_mut(), event).into_iter().collect();
1671 for action in actions {
1672 self.dispatch_collect(action.clone());
1673 all_actions.push(action);
1674 }
1675 }
1676 all_actions
1677 }
1678
1679 pub fn assert_state<F>(&self, predicate: F)
1683 where
1684 F: FnOnce(&S) -> bool,
1685 {
1686 assert!(predicate(self.state()), "State assertion failed");
1687 }
1688
1689 pub fn assert_state_msg<F>(&self, predicate: F, msg: &str)
1691 where
1692 F: FnOnce(&S) -> bool,
1693 {
1694 assert!(predicate(self.state()), "{}", msg);
1695 }
1696
1697 fn ensure_render(&mut self, width: u16, height: u16) {
1700 if self.render.is_none() || self.render.as_ref().map(|r| r.size()) != Some((width, height))
1701 {
1702 self.render = Some(RenderHarness::new(width, height));
1703 }
1704 }
1705
1706 pub fn render<F>(&mut self, width: u16, height: u16, render_fn: F) -> String
1708 where
1709 F: FnOnce(&mut ratatui::Frame, ratatui::layout::Rect, &S),
1710 {
1711 self.ensure_render(width, height);
1712 let store = &self.store;
1713 let render = self.render.as_mut().unwrap();
1714 render.render_to_string(|frame| {
1715 let area = frame.area();
1716 render_fn(frame, area, store.state());
1717 })
1718 }
1719
1720 pub fn render_plain<F>(&mut self, width: u16, height: u16, render_fn: F) -> String
1722 where
1723 F: FnOnce(&mut ratatui::Frame, ratatui::layout::Rect, &S),
1724 {
1725 self.ensure_render(width, height);
1726 let store = &self.store;
1727 let render = self.render.as_mut().unwrap();
1728 render.render_to_string_plain(|frame| {
1729 let area = frame.area();
1730 render_fn(frame, area, store.state());
1731 })
1732 }
1733
1734 pub fn render_default<F>(&mut self, render_fn: F) -> String
1736 where
1737 F: FnOnce(&mut ratatui::Frame, ratatui::layout::Rect, &S),
1738 {
1739 let (w, h) = self.default_size;
1740 self.render(w, h, render_fn)
1741 }
1742
1743 pub fn render_default_plain<F>(&mut self, render_fn: F) -> String
1745 where
1746 F: FnOnce(&mut ratatui::Frame, ratatui::layout::Rect, &S),
1747 {
1748 let (w, h) = self.default_size;
1749 self.render_plain(w, h, render_fn)
1750 }
1751}
1752
1753pub trait EffectAssertions<E> {
1772 fn effects_empty(&self);
1774 fn effects_not_empty(&self);
1776 fn effects_count(&self, n: usize);
1778 fn effects_any_matches<F: Fn(&E) -> bool>(&self, f: F);
1780 fn effects_first_matches<F: Fn(&E) -> bool>(&self, f: F);
1782 fn effects_all_match<F: Fn(&E) -> bool>(&self, f: F);
1784 fn effects_none_match<F: Fn(&E) -> bool>(&self, f: F);
1786}
1787
1788impl<E: std::fmt::Debug> EffectAssertions<E> for Vec<E> {
1789 fn effects_empty(&self) {
1790 assert!(self.is_empty(), "Expected no effects, got {:?}", self);
1791 }
1792
1793 fn effects_not_empty(&self) {
1794 assert!(!self.is_empty(), "Expected effects, got none");
1795 }
1796
1797 fn effects_count(&self, n: usize) {
1798 assert_eq!(
1799 self.len(),
1800 n,
1801 "Expected {} effects, got {}: {:?}",
1802 n,
1803 self.len(),
1804 self
1805 );
1806 }
1807
1808 fn effects_any_matches<F: Fn(&E) -> bool>(&self, f: F) {
1809 assert!(
1810 self.iter().any(&f),
1811 "No effect matched predicate in {:?}",
1812 self
1813 );
1814 }
1815
1816 fn effects_first_matches<F: Fn(&E) -> bool>(&self, f: F) {
1817 let first = self.first().expect("Expected at least one effect");
1818 assert!(f(first), "First effect {:?} did not match predicate", first);
1819 }
1820
1821 fn effects_all_match<F: Fn(&E) -> bool>(&self, f: F) {
1822 for (i, e) in self.iter().enumerate() {
1823 assert!(f(e), "Effect at index {} did not match: {:?}", i, e);
1824 }
1825 }
1826
1827 fn effects_none_match<F: Fn(&E) -> bool>(&self, f: F) {
1828 for (i, e) in self.iter().enumerate() {
1829 assert!(!f(e), "Effect at index {} unexpectedly matched: {:?}", i, e);
1830 }
1831 }
1832}
1833
1834pub trait EffectAssertionsEq<E> {
1839 fn effects_contains(&self, expected: E);
1841 fn effects_first_eq(&self, expected: E);
1843 fn effects_last_eq(&self, expected: E);
1845}
1846
1847impl<E: PartialEq + std::fmt::Debug> EffectAssertionsEq<E> for Vec<E> {
1848 fn effects_contains(&self, expected: E) {
1849 assert!(
1850 self.contains(&expected),
1851 "Expected to contain {:?}, got {:?}",
1852 expected,
1853 self
1854 );
1855 }
1856
1857 fn effects_first_eq(&self, expected: E) {
1858 let first = self.first().expect("Expected at least one effect");
1859 assert_eq!(first, &expected, "First effect mismatch");
1860 }
1861
1862 fn effects_last_eq(&self, expected: E) {
1863 let last = self.last().expect("Expected at least one effect");
1864 assert_eq!(last, &expected, "Last effect mismatch");
1865 }
1866}
1867
1868#[cfg(feature = "testing-time")]
1898pub fn pause_time() {
1899 tokio::time::pause();
1900}
1901
1902#[cfg(feature = "testing-time")]
1904pub fn resume_time() {
1905 tokio::time::resume();
1906}
1907
1908#[cfg(feature = "testing-time")]
1912pub async fn advance_time(duration: std::time::Duration) {
1913 tokio::time::advance(duration).await;
1914}
1915
1916#[cfg(test)]
1917mod tests {
1918 use super::*;
1919
1920 #[test]
1921 fn test_key_simple() {
1922 let k = key("q");
1923 assert_eq!(k.code, KeyCode::Char('q'));
1924 assert_eq!(k.modifiers, KeyModifiers::empty());
1925 }
1926
1927 #[test]
1928 fn test_key_with_ctrl() {
1929 let k = key("ctrl+p");
1930 assert_eq!(k.code, KeyCode::Char('p'));
1931 assert!(k.modifiers.contains(KeyModifiers::CONTROL));
1932 }
1933
1934 #[test]
1935 fn test_key_special() {
1936 let k = key("esc");
1937 assert_eq!(k.code, KeyCode::Esc);
1938
1939 let k = key("enter");
1940 assert_eq!(k.code, KeyCode::Enter);
1941
1942 let k = key("shift+tab");
1943 assert_eq!(k.code, KeyCode::BackTab);
1944 }
1945
1946 #[test]
1947 fn test_char_key() {
1948 let k = char_key('x');
1949 assert_eq!(k.code, KeyCode::Char('x'));
1950 assert_eq!(k.modifiers, KeyModifiers::empty());
1951 }
1952
1953 #[test]
1954 fn test_ctrl_key() {
1955 let k = ctrl_key('c');
1956 assert_eq!(k.code, KeyCode::Char('c'));
1957 assert!(k.modifiers.contains(KeyModifiers::CONTROL));
1958 }
1959
1960 #[derive(Clone, Debug, PartialEq)]
1961 enum TestAction {
1962 Foo,
1963 Bar(i32),
1964 }
1965
1966 impl crate::Action for TestAction {
1967 fn name(&self) -> &'static str {
1968 match self {
1969 TestAction::Foo => "Foo",
1970 TestAction::Bar(_) => "Bar",
1971 }
1972 }
1973 }
1974
1975 #[test]
1976 fn test_harness_emit_and_drain() {
1977 let mut harness = TestHarness::<(), TestAction>::new(());
1978
1979 harness.emit(TestAction::Foo);
1980 harness.emit(TestAction::Bar(42));
1981
1982 let actions = harness.drain_emitted();
1983 assert_eq!(actions.len(), 2);
1984 assert_eq!(actions[0], TestAction::Foo);
1985 assert_eq!(actions[1], TestAction::Bar(42));
1986
1987 let actions = harness.drain_emitted();
1989 assert!(actions.is_empty());
1990 }
1991
1992 #[test]
1993 fn test_assert_macros() {
1994 let actions = vec![TestAction::Foo, TestAction::Bar(42)];
1995
1996 assert_emitted!(actions, TestAction::Foo);
1997 assert_emitted!(actions, TestAction::Bar(42));
1998 assert_emitted!(actions, TestAction::Bar(_));
1999
2000 assert_not_emitted!(actions, TestAction::Bar(99));
2001
2002 let found = find_emitted!(actions, TestAction::Bar(_));
2003 assert!(found.is_some());
2004
2005 let count = count_emitted!(actions, TestAction::Bar(_));
2006 assert_eq!(count, 1);
2007 }
2008
2009 #[test]
2011 fn test_action_assertions_first_last() {
2012 let actions = vec![TestAction::Foo, TestAction::Bar(42), TestAction::Bar(99)];
2013
2014 actions.assert_first(TestAction::Foo);
2015 actions.assert_last(TestAction::Bar(99));
2016 }
2017
2018 #[test]
2019 fn test_action_assertions_contains() {
2020 let actions = vec![TestAction::Foo, TestAction::Bar(42)];
2021
2022 actions.assert_contains(TestAction::Foo);
2023 actions.assert_contains(TestAction::Bar(42));
2024 actions.assert_not_contains(TestAction::Bar(99));
2025 }
2026
2027 #[test]
2028 fn test_action_assertions_empty() {
2029 let empty: Vec<TestAction> = vec![];
2030 let non_empty = vec![TestAction::Foo];
2031
2032 empty.assert_empty();
2033 non_empty.assert_not_empty();
2034 }
2035
2036 #[test]
2037 fn test_action_assertions_count() {
2038 let actions = vec![TestAction::Foo, TestAction::Bar(1), TestAction::Bar(2)];
2039 actions.assert_count(3);
2040 }
2041
2042 #[test]
2043 fn test_action_assertions_matches() {
2044 let actions = vec![TestAction::Foo, TestAction::Bar(42), TestAction::Bar(99)];
2045
2046 actions.assert_first_matches(|a| matches!(a, TestAction::Foo));
2047 actions.assert_any_matches(|a| matches!(a, TestAction::Bar(x) if *x > 50));
2048 actions.assert_all_match(|a| matches!(a, TestAction::Foo | TestAction::Bar(_)));
2049 actions.assert_none_match(|a| matches!(a, TestAction::Bar(0)));
2050 }
2051
2052 #[test]
2054 fn test_keys_multiple() {
2055 let k = keys("a b c");
2056 assert_eq!(k.len(), 3);
2057 assert_eq!(k[0].code, KeyCode::Char('a'));
2058 assert_eq!(k[1].code, KeyCode::Char('b'));
2059 assert_eq!(k[2].code, KeyCode::Char('c'));
2060 }
2061
2062 #[test]
2063 fn test_keys_with_modifiers() {
2064 let k = keys("ctrl+c esc enter");
2065 assert_eq!(k.len(), 3);
2066 assert_eq!(k[0].code, KeyCode::Char('c'));
2067 assert!(k[0].modifiers.contains(KeyModifiers::CONTROL));
2068 assert_eq!(k[1].code, KeyCode::Esc);
2069 assert_eq!(k[2].code, KeyCode::Enter);
2070 }
2071
2072 #[test]
2074 fn test_render_harness_new() {
2075 let harness = RenderHarness::new(80, 24);
2076 assert_eq!(harness.size(), (80, 24));
2077 }
2078
2079 #[test]
2080 fn test_render_harness_render_plain() {
2081 let mut harness = RenderHarness::new(10, 2);
2082 let output = harness.render_to_string_plain(|frame| {
2083 use ratatui::widgets::Paragraph;
2084 let p = Paragraph::new("Hello");
2085 frame.render_widget(p, frame.area());
2086 });
2087
2088 assert!(output.starts_with("Hello"));
2090 }
2091
2092 #[test]
2093 fn test_render_harness_resize() {
2094 let mut harness = RenderHarness::new(80, 24);
2095 assert_eq!(harness.size(), (80, 24));
2096
2097 harness.resize(100, 30);
2098 assert_eq!(harness.size(), (100, 30));
2099 }
2100
2101 #[test]
2103 fn test_complete_action() {
2104 let mut harness = TestHarness::<(), TestAction>::new(());
2105
2106 harness.complete_action(TestAction::Foo);
2107 harness.complete_actions([TestAction::Bar(1), TestAction::Bar(2)]);
2108
2109 let actions = harness.drain_emitted();
2110 assert_eq!(actions.len(), 3);
2111 actions.assert_first(TestAction::Foo);
2112 }
2113
2114 #[derive(Default, Debug, PartialEq)]
2116 struct TestState {
2117 count: i32,
2118 name: String,
2119 }
2120
2121 #[test]
2122 fn test_assert_state_macro() {
2123 let harness = TestHarness::<TestState, TestAction>::new(TestState {
2124 count: 42,
2125 name: "test".to_string(),
2126 });
2127
2128 assert_state!(harness, count, 42);
2129 assert_state!(harness, name, "test".to_string());
2130 }
2131}