tui_dispatch_core/
testing.rs

1//! Test utilities for tui-dispatch applications
2//!
3//! This module provides helpers for testing TUI applications built with tui-dispatch:
4//!
5//! - [`key`]: Create `KeyEvent` from string (e.g., `key("ctrl+p")`)
6//! - [`key_events`]: Create multiple `Event`s from space-separated key string
7//! - [`TestHarness`]: Generic test harness with action channel and state management
8//! - [`ActionAssertions`]: Fluent assertion trait for action vectors
9//! - Assertion macros for verifying emitted actions
10//!
11//! # Example
12//!
13//! ```ignore
14//! use tui_dispatch::testing::{key, TestHarness, ActionAssertions};
15//!
16//! #[derive(Clone, Debug, PartialEq)]
17//! enum Action {
18//!     Increment,
19//!     Decrement,
20//! }
21//!
22//! let mut harness = TestHarness::<i32, Action>::new(0);
23//!
24//! // Emit and check actions with fluent API
25//! harness.emit(Action::Decrement);
26//! harness.emit(Action::Increment);
27//! let emitted = harness.drain_emitted();
28//! emitted.assert_first(Action::Decrement);
29//! emitted.assert_contains(Action::Increment);
30//! emitted.assert_count(2);
31//! ```
32
33use 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
42// ============================================================================
43// Fluent Action Assertions
44// ============================================================================
45
46/// Fluent assertion trait for action vectors.
47///
48/// This trait only requires `Debug`, making it usable with any action type.
49/// For equality-based assertions (like `assert_first`), use [`ActionAssertionsEq`].
50///
51/// # Example
52///
53/// ```ignore
54/// use tui_dispatch::testing::ActionAssertions;
55///
56/// let actions = harness.drain_emitted();
57/// actions.assert_not_empty();
58/// actions.assert_count(3);
59/// actions.assert_any_matches(|a| matches!(a, Action::SelectKey(i) if *i > 0));
60/// ```
61pub trait ActionAssertions<A> {
62    /// Assert that the vector is empty.
63    ///
64    /// # Panics
65    /// Panics if the vector is not empty.
66    fn assert_empty(&self);
67
68    /// Assert that the vector is not empty.
69    ///
70    /// # Panics
71    /// Panics if the vector is empty.
72    fn assert_not_empty(&self);
73
74    /// Assert that the vector has exactly `n` elements.
75    ///
76    /// # Panics
77    /// Panics if the count doesn't match.
78    fn assert_count(&self, n: usize);
79
80    /// Assert that the first action matches a predicate.
81    ///
82    /// # Panics
83    /// Panics if the vector is empty or the predicate returns false.
84    fn assert_first_matches<F: Fn(&A) -> bool>(&self, f: F);
85
86    /// Assert that any action matches a predicate.
87    ///
88    /// # Panics
89    /// Panics if no action matches the predicate.
90    fn assert_any_matches<F: Fn(&A) -> bool>(&self, f: F);
91
92    /// Assert that all actions match a predicate.
93    ///
94    /// # Panics
95    /// Panics if any action doesn't match the predicate.
96    fn assert_all_match<F: Fn(&A) -> bool>(&self, f: F);
97
98    /// Assert that no action matches a predicate.
99    ///
100    /// # Panics
101    /// Panics if any action matches the predicate.
102    fn assert_none_match<F: Fn(&A) -> bool>(&self, f: F);
103}
104
105/// Equality-based assertions for action vectors.
106///
107/// This trait requires `PartialEq` for equality comparisons.
108/// For predicate-based assertions that don't need `PartialEq`, use [`ActionAssertions`].
109///
110/// # Example
111///
112/// ```ignore
113/// use tui_dispatch::testing::ActionAssertionsEq;
114///
115/// let actions = harness.drain_emitted();
116/// actions.assert_first(Action::StartSearch);
117/// actions.assert_contains(Action::SelectKey(42));
118/// ```
119pub trait ActionAssertionsEq<A> {
120    /// Assert that the first action equals the expected value.
121    ///
122    /// # Panics
123    /// Panics if the vector is empty or the first action doesn't match.
124    fn assert_first(&self, expected: A);
125
126    /// Assert that the last action equals the expected value.
127    ///
128    /// # Panics
129    /// Panics if the vector is empty or the last action doesn't match.
130    fn assert_last(&self, expected: A);
131
132    /// Assert that the vector contains the expected action.
133    ///
134    /// # Panics
135    /// Panics if no action matches the expected value.
136    fn assert_contains(&self, expected: A);
137
138    /// Assert that the vector does not contain the expected action.
139    ///
140    /// # Panics
141    /// Panics if any action matches the expected value.
142    fn assert_not_contains(&self, expected: A);
143}
144
145// ActionAssertions impl for Vec - only requires Debug
146impl<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
216// ActionAssertionsEq impl for Vec - requires PartialEq + Debug
217impl<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
264// ActionAssertions impl for slices - only requires Debug
265impl<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
335// ActionAssertionsEq impl for slices - requires PartialEq + Debug
336impl<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
383// ============================================================================
384// Key Event Helpers
385// ============================================================================
386
387/// Create a `KeyEvent` from a key string.
388///
389/// This is a convenience wrapper around [`parse_key_string`] that panics
390/// if the key string is invalid, making it suitable for use in tests.
391///
392/// # Examples
393///
394/// ```
395/// use tui_dispatch_core::testing::key;
396/// use crossterm::event::{KeyCode, KeyModifiers};
397///
398/// let k = key("q");
399/// assert_eq!(k.code, KeyCode::Char('q'));
400///
401/// let k = key("ctrl+p");
402/// assert_eq!(k.code, KeyCode::Char('p'));
403/// assert!(k.modifiers.contains(KeyModifiers::CONTROL));
404///
405/// let k = key("shift+tab");
406/// assert_eq!(k.code, KeyCode::BackTab);
407/// ```
408///
409/// # Panics
410///
411/// Panics if the key string cannot be parsed.
412pub fn key(s: &str) -> KeyEvent {
413    parse_key_string(s).unwrap_or_else(|| panic!("Invalid key string: {:?}", s))
414}
415
416/// Create a `KeyEvent` for a character with no modifiers.
417///
418/// # Examples
419///
420/// ```
421/// use tui_dispatch_core::testing::char_key;
422/// use crossterm::event::KeyCode;
423///
424/// let k = char_key('x');
425/// assert_eq!(k.code, KeyCode::Char('x'));
426/// ```
427pub 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
436/// Create a `KeyEvent` for a character with Ctrl modifier.
437pub 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
446/// Create a `KeyEvent` for a character with Alt modifier.
447pub 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
456/// Create an `Event<C>` containing a key event from a key string.
457///
458/// This is useful for testing component `handle_event` methods.
459///
460/// # Examples
461///
462/// ```ignore
463/// use tui_dispatch::testing::key_event;
464///
465/// let event = key_event::<MyComponentId>("ctrl+p");
466/// let actions = component.handle_event(&event, props);
467/// ```
468pub fn key_event<C: ComponentId>(s: &str) -> Event<C> {
469    Event {
470        kind: EventKind::Key(key(s)),
471        context: EventContext::default(),
472    }
473}
474
475/// Create an `Event<C>` from a `KeyEvent`.
476///
477/// # Examples
478///
479/// ```ignore
480/// use tui_dispatch::testing::{key, into_event};
481///
482/// let k = key("enter");
483/// let event = into_event::<MyComponentId>(k);
484/// ```
485pub 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
492/// Create multiple `Event<C>` from a space-separated key string.
493///
494/// This is useful for simulating key sequences in tests.
495///
496/// # Examples
497///
498/// ```ignore
499/// use tui_dispatch::testing::key_events;
500///
501/// // Single key
502/// let events = key_events::<MyComponentId>("ctrl+p");
503/// assert_eq!(events.len(), 1);
504///
505/// // Multiple keys separated by spaces
506/// let events = key_events::<MyComponentId>("ctrl+p down down enter");
507/// assert_eq!(events.len(), 4);
508///
509/// // Type characters
510/// let events = key_events::<MyComponentId>("h e l l o");
511/// assert_eq!(events.len(), 5);
512/// ```
513///
514/// # Panics
515///
516/// Panics if any key string in the sequence cannot be parsed.
517pub fn key_events<C: ComponentId>(keys: &str) -> Vec<Event<C>> {
518    keys.split_whitespace().map(|k| key_event::<C>(k)).collect()
519}
520
521/// Parse multiple key strings into `KeyEvent`s.
522///
523/// Similar to [`key_events`] but returns raw `KeyEvent`s instead of `Event<C>`.
524///
525/// # Examples
526///
527/// ```
528/// use tui_dispatch_core::testing::keys;
529/// use crossterm::event::KeyCode;
530///
531/// let key_events = keys("ctrl+c esc enter");
532/// assert_eq!(key_events.len(), 3);
533/// assert_eq!(key_events[0].code, KeyCode::Char('c'));
534/// assert_eq!(key_events[1].code, KeyCode::Esc);
535/// assert_eq!(key_events[2].code, KeyCode::Enter);
536/// ```
537pub fn keys(key_str: &str) -> Vec<KeyEvent> {
538    key_str.split_whitespace().map(key).collect()
539}
540
541/// Generic test harness for tui-dispatch applications.
542///
543/// Provides:
544/// - State management with a simple `state` field
545/// - Action channel for capturing emitted actions
546/// - Helper methods for dispatching and draining actions
547///
548/// # Type Parameters
549///
550/// - `S`: The state type
551/// - `A`: The action type (must implement [`Action`])
552///
553/// # Example
554///
555/// ```ignore
556/// use tui_dispatch::testing::TestHarness;
557///
558/// #[derive(Clone, Debug, PartialEq)]
559/// enum MyAction { Foo, Bar(i32) }
560///
561/// let mut harness = TestHarness::<MyState, MyAction>::new(MyState::default());
562///
563/// // Emit actions (simulating what handlers would do)
564/// harness.emit(MyAction::Foo);
565/// harness.emit(MyAction::Bar(42));
566///
567/// // Drain and verify
568/// let actions = harness.drain_emitted();
569/// assert_eq!(actions.len(), 2);
570/// ```
571pub struct TestHarness<S, A: Action> {
572    /// The application state under test
573    pub state: S,
574    /// Sender for emitting actions
575    tx: mpsc::UnboundedSender<A>,
576    /// Receiver for draining emitted actions
577    rx: mpsc::UnboundedReceiver<A>,
578}
579
580impl<S, A: Action> TestHarness<S, A> {
581    /// Create a new test harness with the given initial state.
582    pub fn new(state: S) -> Self {
583        let (tx, rx) = mpsc::unbounded_channel();
584        Self { state, tx, rx }
585    }
586
587    /// Get a clone of the action sender for passing to handlers.
588    pub fn sender(&self) -> mpsc::UnboundedSender<A> {
589        self.tx.clone()
590    }
591
592    /// Emit an action (simulates what a handler would do).
593    pub fn emit(&self, action: A) {
594        let _ = self.tx.send(action);
595    }
596
597    /// Drain all emitted actions from the channel.
598    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    /// Check if any actions were emitted.
607    pub fn has_emitted(&mut self) -> bool {
608        !self.drain_emitted().is_empty()
609    }
610
611    /// Simulate async action completion (semantic alias for [`Self::emit`]).
612    ///
613    /// Use this when simulating backend responses or async operation results.
614    ///
615    /// # Example
616    ///
617    /// ```ignore
618    /// use tui_dispatch::testing::TestHarness;
619    ///
620    /// harness.complete_action(Action::DidConnect { id: "conn-1", .. });
621    /// harness.complete_action(Action::DidScanKeys { keys: vec!["foo", "bar"] });
622    /// ```
623    pub fn complete_action(&self, action: A) {
624        self.emit(action);
625    }
626
627    /// Simulate multiple async action completions.
628    ///
629    /// # Example
630    ///
631    /// ```ignore
632    /// harness.complete_actions([
633    ///     Action::DidConnect { id: "conn-1" },
634    ///     Action::DidLoadValue { key: "foo", value: "bar" },
635    /// ]);
636    /// ```
637    pub fn complete_actions(&self, actions: impl IntoIterator<Item = A>) {
638        for action in actions {
639            self.emit(action);
640        }
641    }
642
643    /// Send a sequence of key events and collect actions from a handler.
644    ///
645    /// Parses the space-separated key string and calls the handler for each event,
646    /// collecting all returned actions.
647    ///
648    /// # Example
649    ///
650    /// ```ignore
651    /// use tui_dispatch::testing::TestHarness;
652    ///
653    /// let mut harness = TestHarness::<AppState, Action>::new(AppState::default());
654    ///
655    /// // Send key sequence and collect actions
656    /// let actions = harness.send_keys("ctrl+p down down enter", |state, event| {
657    ///     component.handle_event(&event.kind, ComponentProps { state })
658    /// });
659    ///
660    /// actions.assert_contains(Action::SelectItem(2));
661    /// ```
662    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    /// Send a sequence of key events, calling handler and emitting returned actions.
678    ///
679    /// Unlike [`Self::send_keys`], this method emits returned actions to the harness channel,
680    /// allowing you to drain them later.
681    ///
682    /// # Example
683    ///
684    /// ```ignore
685    /// harness.send_keys_emit("ctrl+p down enter", |state, event| {
686    ///     component.handle_event(&event.kind, props)
687    /// });
688    ///
689    /// let actions = harness.drain_emitted();
690    /// actions.assert_contains(Action::Confirm);
691    /// ```
692    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
714/// Category-aware methods for TestHarness.
715///
716/// These methods are available when the action type implements [`ActionCategory`],
717/// enabling filtering and assertions by action category.
718impl<S, A: ActionCategory> TestHarness<S, A> {
719    /// Drain all emitted actions that belong to a specific category.
720    ///
721    /// Actions not matching the category remain in the channel for later draining.
722    ///
723    /// # Example
724    ///
725    /// ```ignore
726    /// use tui_dispatch::testing::TestHarness;
727    ///
728    /// let mut harness = TestHarness::<MyState, MyAction>::new(MyState::default());
729    ///
730    /// // Emit various actions
731    /// harness.emit(MyAction::SearchStart);
732    /// harness.emit(MyAction::ConnectionFormOpen);
733    /// harness.emit(MyAction::SearchClear);
734    ///
735    /// // Drain only search-related actions
736    /// let search_actions = harness.drain_category("search");
737    /// assert_eq!(search_actions.len(), 2);
738    ///
739    /// // Other actions remain
740    /// let remaining = harness.drain_emitted();
741    /// assert_eq!(remaining.len(), 1);
742    /// ```
743    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        // Re-emit non-matching actions
757        for action in non_matching {
758            let _ = self.tx.send(action);
759        }
760
761        matching
762    }
763
764    /// Check if any action of the given category was emitted.
765    ///
766    /// This drains only the matching category, leaving other actions in the channel.
767    pub fn has_category(&mut self, category: &str) -> bool {
768        !self.drain_category(category).is_empty()
769    }
770}
771
772/// Assert that a specific action was emitted.
773///
774/// # Example
775///
776/// ```ignore
777/// use tui_dispatch::testing::assert_emitted;
778///
779/// let actions = harness.drain_emitted();
780/// assert_emitted!(actions, Action::Increment);
781/// assert_emitted!(actions, Action::SetValue(42));
782/// ```
783#[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/// Assert that a specific action was NOT emitted.
796///
797/// # Example
798///
799/// ```ignore
800/// use tui_dispatch::testing::assert_not_emitted;
801///
802/// let actions = harness.drain_emitted();
803/// assert_not_emitted!(actions, Action::Quit);
804/// ```
805#[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/// Find and return the first action matching a pattern.
818///
819/// # Example
820///
821/// ```ignore
822/// use tui_dispatch::testing::find_emitted;
823///
824/// let actions = harness.drain_emitted();
825/// if let Some(Action::SetValue(v)) = find_emitted!(actions, Action::SetValue(_)) {
826///     assert_eq!(*v, 42);
827/// }
828/// ```
829#[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/// Count how many actions match a pattern.
837///
838/// # Example
839///
840/// ```ignore
841/// use tui_dispatch::testing::count_emitted;
842///
843/// let actions = harness.drain_emitted();
844/// assert_eq!(count_emitted!(actions, Action::Tick), 3);
845/// ```
846#[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/// Assert that an action of a specific category was emitted.
854///
855/// This requires the action type to implement [`ActionCategory`].
856///
857/// # Example
858///
859/// ```ignore
860/// use tui_dispatch::testing::assert_category_emitted;
861///
862/// let actions = harness.drain_emitted();
863/// assert_category_emitted!(actions, "search");
864/// assert_category_emitted!(actions, "connection_form");
865/// ```
866#[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/// Assert that NO action of a specific category was emitted.
882///
883/// This requires the action type to implement [`ActionCategory`].
884///
885/// # Example
886///
887/// ```ignore
888/// use tui_dispatch::testing::assert_category_not_emitted;
889///
890/// let actions = harness.drain_emitted();
891/// assert_category_not_emitted!(actions, "search");
892/// ```
893#[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/// Count how many actions belong to a specific category.
915///
916/// This requires the action type to implement [`ActionCategory`].
917///
918/// # Example
919///
920/// ```ignore
921/// use tui_dispatch::testing::count_category;
922///
923/// let actions = harness.drain_emitted();
924/// assert_eq!(count_category!(actions, "search"), 3);
925/// ```
926#[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// ============================================================================
938// State Assertions
939// ============================================================================
940
941/// Assert that a field of the harness state has an expected value.
942///
943/// # Example
944///
945/// ```ignore
946/// use tui_dispatch::testing::{TestHarness, assert_state};
947///
948/// let harness = TestHarness::<AppState, Action>::new(AppState::default());
949/// assert_state!(harness, counter, 0);
950/// assert_state!(harness, ui.focused_panel, Panel::Keys);
951/// ```
952#[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/// Assert that a field of the harness state matches a pattern.
967///
968/// # Example
969///
970/// ```ignore
971/// use tui_dispatch::testing::{TestHarness, assert_state_matches};
972///
973/// assert_state_matches!(harness, connection_status, ConnectionStatus::Connected { .. });
974/// ```
975#[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
988// ============================================================================
989// Render Harness
990// ============================================================================
991
992use ratatui::backend::{Backend, TestBackend};
993use ratatui::buffer::Buffer;
994use ratatui::Terminal;
995
996/// Test harness for capturing rendered output.
997///
998/// Provides utilities for rendering components to a test buffer and
999/// converting the output to strings for snapshot testing.
1000///
1001/// # Example
1002///
1003/// ```ignore
1004/// use tui_dispatch::testing::RenderHarness;
1005///
1006/// let mut render = RenderHarness::new(80, 24);
1007///
1008/// // Render a component
1009/// let output = render.render_to_string(|frame| {
1010///     my_component.render(frame, frame.area(), props);
1011/// });
1012///
1013/// // Use with insta for snapshot testing
1014/// insta::assert_snapshot!(output);
1015/// ```
1016pub struct RenderHarness {
1017    terminal: Terminal<TestBackend>,
1018}
1019
1020impl RenderHarness {
1021    /// Create a new render harness with the specified dimensions.
1022    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    /// Render using the provided function and return the buffer.
1029    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    /// Render and convert the buffer to a string representation.
1040    ///
1041    /// The output includes ANSI escape codes for colors and styles.
1042    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    /// Render and convert to a plain string (no ANSI codes).
1051    ///
1052    /// Useful for simple text assertions without style information.
1053    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    /// Get the current terminal size.
1062    pub fn size(&self) -> (u16, u16) {
1063        let area = self.terminal.backend().size().unwrap_or_default();
1064        (area.width, area.height)
1065    }
1066
1067    /// Resize the terminal.
1068    pub fn resize(&mut self, width: u16, height: u16) {
1069        self.terminal.backend_mut().resize(width, height);
1070    }
1071}
1072
1073/// Convert a ratatui Buffer to a string with ANSI escape codes.
1074///
1075/// Each cell's foreground color, background color, and modifiers are
1076/// converted to ANSI escape sequences.
1077pub 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            // Start with reset
1089            let _ = write!(result, "\x1b[0m");
1090
1091            // Foreground color
1092            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            // Background color
1119            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            // Modifiers
1146            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
1173/// Convert a ratatui Buffer to a plain string (no ANSI codes).
1174///
1175/// Only extracts the text content, ignoring colors and styles.
1176pub 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
1191/// Convert a specific rect of a buffer to a plain string.
1192///
1193/// Useful for testing a specific region of the rendered output.
1194pub 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
1210// ============================================================================
1211// StoreTestHarness - Integrated Store + Test Harness
1212// ============================================================================
1213
1214/// Test harness combining Store + action channel + render capabilities.
1215///
1216/// Provides an integrated testing experience for applications using the
1217/// standard [`Store`](crate::Store) with bool reducers.
1218///
1219/// # Example
1220///
1221/// ```ignore
1222/// use tui_dispatch::testing::StoreTestHarness;
1223///
1224/// let mut harness = StoreTestHarness::new(AppState::default(), reducer);
1225///
1226/// // Dispatch and check state
1227/// harness.dispatch(Action::Increment);
1228/// harness.assert_state(|s| s.count == 1);
1229///
1230/// // Send keys through component
1231/// let actions = harness.send_keys::<NumericComponentId, _, _>("j j enter", |state, event| {
1232///     component.handle_event(&event.kind, Props { state })
1233/// });
1234/// actions.assert_contains(Action::Select(2));
1235///
1236/// // Snapshot testing
1237/// let output = harness.render_plain(60, 24, |f, area, state| {
1238///     component.render(f, area, Props { state });
1239/// });
1240/// assert!(output.contains("expected text"));
1241/// ```
1242pub 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    /// Create a new harness with initial state and reducer.
1252    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    /// Set the default terminal size for rendering.
1264    pub fn with_size(mut self, width: u16, height: u16) -> Self {
1265        self.default_size = (width, height);
1266        self
1267    }
1268
1269    // === Store Operations ===
1270
1271    /// Dispatch an action to the store.
1272    ///
1273    /// Returns `true` if the state changed.
1274    pub fn dispatch(&mut self, action: A) -> bool {
1275        self.store.dispatch(action)
1276    }
1277
1278    /// Dispatch multiple actions in sequence.
1279    ///
1280    /// Returns a vector of booleans indicating which dispatches changed state.
1281    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    /// Get a reference to the current state.
1286    pub fn state(&self) -> &S {
1287        self.store.state()
1288    }
1289
1290    /// Get a mutable reference to the state for test setup.
1291    pub fn state_mut(&mut self) -> &mut S {
1292        self.store.state_mut()
1293    }
1294
1295    // === Action Channel (for async simulation) ===
1296
1297    /// Get a sender clone for simulating async action completions.
1298    pub fn sender(&self) -> mpsc::UnboundedSender<A> {
1299        self.tx.clone()
1300    }
1301
1302    /// Emit an action to the channel (simulates async completion).
1303    pub fn emit(&self, action: A) {
1304        let _ = self.tx.send(action);
1305    }
1306
1307    /// Simulate async action completion (semantic alias for [`Self::emit`]).
1308    pub fn complete_action(&self, action: A) {
1309        self.emit(action);
1310    }
1311
1312    /// Simulate multiple async action completions.
1313    pub fn complete_actions(&self, actions: impl IntoIterator<Item = A>) {
1314        for action in actions {
1315            self.emit(action);
1316        }
1317    }
1318
1319    /// Drain all emitted actions from the channel.
1320    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    /// Check if any actions were emitted.
1329    pub fn has_emitted(&mut self) -> bool {
1330        !self.drain_emitted().is_empty()
1331    }
1332
1333    /// Process all emitted actions through the store.
1334    ///
1335    /// Drains the channel and dispatches each action to the store.
1336    /// Returns `(changed_count, total_count)`.
1337    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    // === Key/Event Handling ===
1348
1349    /// Send a sequence of key events and collect actions from a handler.
1350    ///
1351    /// Parses the space-separated key string and calls the handler for each event,
1352    /// collecting all returned actions.
1353    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    /// Send keys and dispatch returned actions to the store.
1369    ///
1370    /// Returns the actions that were dispatched.
1371    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    // === State Assertions ===
1390
1391    /// Assert a condition on the current state.
1392    ///
1393    /// # Panics
1394    ///
1395    /// Panics if the predicate returns `false`.
1396    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    /// Assert a condition with a custom message.
1404    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    // === Render Operations ===
1412
1413    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    /// Render using the provided function, returns string with ANSI codes.
1421    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    /// Render to plain string (no ANSI codes).
1435    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    /// Render with default terminal size (set via [`Self::with_size`]).
1449    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    /// Render with default size to plain string.
1458    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
1473// ============================================================================
1474// EffectStoreTestHarness - Integrated EffectStore + Test Harness
1475// ============================================================================
1476
1477/// Test harness for effect-based stores.
1478///
1479/// Similar to [`StoreTestHarness`] but for applications using
1480/// [`EffectStore`](crate::EffectStore) with [`DispatchResult`](crate::DispatchResult) reducers.
1481///
1482/// # Example
1483///
1484/// ```ignore
1485/// use tui_dispatch::testing::EffectStoreTestHarness;
1486///
1487/// let mut harness = EffectStoreTestHarness::new(AppState::default(), reducer);
1488///
1489/// // Dispatch and collect effects
1490/// harness.dispatch_collect(Action::WeatherFetch);
1491/// harness.assert_state(|s| s.is_loading);
1492///
1493/// // Check emitted effects
1494/// let effects = harness.drain_effects();
1495/// effects.assert_count(1);
1496/// effects.assert_first_matches(|e| matches!(e, Effect::FetchWeather { .. }));
1497///
1498/// // Simulate async completion
1499/// harness.complete_action(Action::WeatherDidLoad(data));
1500/// harness.process_emitted();
1501/// harness.assert_state(|s| s.weather.is_some());
1502/// ```
1503pub 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    /// Create a new harness with initial state and effect reducer.
1514    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    /// Set the default terminal size for rendering.
1527    pub fn with_size(mut self, width: u16, height: u16) -> Self {
1528        self.default_size = (width, height);
1529        self
1530    }
1531
1532    // === Store Operations ===
1533
1534    /// Dispatch an action to the store.
1535    ///
1536    /// Returns the [`DispatchResult`](crate::DispatchResult) with change status and effects.
1537    pub fn dispatch(&mut self, action: A) -> crate::DispatchResult<E> {
1538        self.store.dispatch(action)
1539    }
1540
1541    /// Dispatch an action and automatically collect its effects.
1542    ///
1543    /// Returns `true` if state changed. Effects are collected internally
1544    /// and can be retrieved with [`Self::drain_effects`].
1545    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    /// Dispatch multiple actions, collecting all effects.
1552    ///
1553    /// Returns a vector of booleans indicating which dispatches changed state.
1554    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    /// Get a reference to the current state.
1562    pub fn state(&self) -> &S {
1563        self.store.state()
1564    }
1565
1566    /// Get a mutable reference to the state for test setup.
1567    pub fn state_mut(&mut self) -> &mut S {
1568        self.store.state_mut()
1569    }
1570
1571    // === Effect Operations ===
1572
1573    /// Drain all collected effects.
1574    pub fn drain_effects(&mut self) -> Vec<E> {
1575        std::mem::take(&mut self.effects)
1576    }
1577
1578    /// Check if any effects were collected.
1579    pub fn has_effects(&self) -> bool {
1580        !self.effects.is_empty()
1581    }
1582
1583    /// Get the number of collected effects.
1584    pub fn effect_count(&self) -> usize {
1585        self.effects.len()
1586    }
1587
1588    // === Action Channel (for async simulation) ===
1589
1590    /// Get a sender clone for simulating async action completions.
1591    pub fn sender(&self) -> mpsc::UnboundedSender<A> {
1592        self.tx.clone()
1593    }
1594
1595    /// Emit an action to the channel (simulates async completion).
1596    pub fn emit(&self, action: A) {
1597        let _ = self.tx.send(action);
1598    }
1599
1600    /// Simulate async action completion (semantic alias for [`Self::emit`]).
1601    pub fn complete_action(&self, action: A) {
1602        self.emit(action);
1603    }
1604
1605    /// Simulate multiple async action completions.
1606    pub fn complete_actions(&self, actions: impl IntoIterator<Item = A>) {
1607        for action in actions {
1608            self.emit(action);
1609        }
1610    }
1611
1612    /// Drain all emitted actions from the channel.
1613    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    /// Check if any actions were emitted.
1622    pub fn has_emitted(&mut self) -> bool {
1623        !self.drain_emitted().is_empty()
1624    }
1625
1626    /// Process all emitted actions through the store, collecting effects.
1627    ///
1628    /// Drains the channel and dispatches each action to the store.
1629    /// Returns `(changed_count, total_count)`.
1630    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    // === Key/Event Handling ===
1641
1642    /// Send a sequence of key events and collect actions from a handler.
1643    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    /// Send keys and dispatch returned actions to the store.
1659    ///
1660    /// Returns the actions that were dispatched. Effects are collected.
1661    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    // === State Assertions ===
1680
1681    /// Assert a condition on the current state.
1682    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    /// Assert a condition with a custom message.
1690    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    // === Render Operations ===
1698
1699    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    /// Render using the provided function, returns string with ANSI codes.
1707    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    /// Render to plain string (no ANSI codes).
1721    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    /// Render with default terminal size.
1735    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    /// Render with default size to plain string.
1744    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
1753// ============================================================================
1754// EffectAssertions - Fluent assertions for effect vectors
1755// ============================================================================
1756
1757/// Fluent assertions for effect vectors.
1758///
1759/// Method names are prefixed with `effects_` to avoid conflicts with
1760/// [`ActionAssertions`] when both traits are in scope.
1761///
1762/// # Example
1763///
1764/// ```ignore
1765/// use tui_dispatch::testing::EffectAssertions;
1766///
1767/// let effects = harness.drain_effects();
1768/// effects.effects_count(1);
1769/// effects.effects_first_matches(|e| matches!(e, Effect::FetchWeather { .. }));
1770/// ```
1771pub trait EffectAssertions<E> {
1772    /// Assert the effect vector is empty.
1773    fn effects_empty(&self);
1774    /// Assert the effect vector is not empty.
1775    fn effects_not_empty(&self);
1776    /// Assert the effect vector has exactly `n` elements.
1777    fn effects_count(&self, n: usize);
1778    /// Assert any effect matches the predicate.
1779    fn effects_any_matches<F: Fn(&E) -> bool>(&self, f: F);
1780    /// Assert the first effect matches the predicate.
1781    fn effects_first_matches<F: Fn(&E) -> bool>(&self, f: F);
1782    /// Assert all effects match the predicate.
1783    fn effects_all_match<F: Fn(&E) -> bool>(&self, f: F);
1784    /// Assert no effects match the predicate.
1785    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
1834/// Equality-based assertions for effect vectors.
1835///
1836/// Method names are prefixed with `effects_` to avoid conflicts with
1837/// [`ActionAssertionsEq`] when both traits are in scope.
1838pub trait EffectAssertionsEq<E> {
1839    /// Assert the vector contains the expected effect.
1840    fn effects_contains(&self, expected: E);
1841    /// Assert the first effect equals the expected.
1842    fn effects_first_eq(&self, expected: E);
1843    /// Assert the last effect equals the expected.
1844    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// ============================================================================
1869// Time Control (Feature-gated)
1870// ============================================================================
1871
1872/// Time control utilities for testing debounced actions.
1873///
1874/// These functions require the `testing-time` feature and must be used
1875/// within a `#[tokio::test]` context.
1876///
1877/// # Example
1878///
1879/// ```ignore
1880/// use tui_dispatch::testing::{pause_time, advance_time};
1881/// use std::time::Duration;
1882///
1883/// #[tokio::test]
1884/// async fn test_debounce() {
1885///     pause_time();
1886///
1887///     // Simulate typing with debounce
1888///     harness.send_keys("a b c", handler);
1889///
1890///     // Advance past debounce threshold
1891///     advance_time(Duration::from_millis(300)).await;
1892///
1893///     let actions = harness.drain_emitted();
1894///     assert_emitted!(actions, Action::DebouncedSearch { .. });
1895/// }
1896/// ```
1897#[cfg(feature = "testing-time")]
1898pub fn pause_time() {
1899    tokio::time::pause();
1900}
1901
1902/// Resume real-time execution after pausing.
1903#[cfg(feature = "testing-time")]
1904pub fn resume_time() {
1905    tokio::time::resume();
1906}
1907
1908/// Advance the paused clock by the specified duration.
1909///
1910/// Must be called after [`pause_time`].
1911#[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        // Drain again should be empty
1988        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    // ActionAssertions trait tests
2010    #[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    // key_events / keys tests
2053    #[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    // RenderHarness tests
2073    #[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        // Should contain "Hello" followed by spaces to fill the width
2089        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    // complete_action tests
2102    #[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    // assert_state! macro test
2115    #[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}