Skip to main content

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
1217/// [`Store`](crate::Store) with [`ReducerResult`](crate::ReducerResult)
1218/// reducers.
1219///
1220/// # Example
1221///
1222/// ```ignore
1223/// use tui_dispatch::testing::StoreTestHarness;
1224///
1225/// let mut harness = StoreTestHarness::new(AppState::default(), reducer);
1226///
1227/// // Dispatch and check state
1228/// harness.dispatch(Action::Increment);
1229/// harness.assert_state(|s| s.count == 1);
1230///
1231/// // Send keys through component
1232/// let actions = harness.send_keys::<NumericComponentId, _, _>("j j enter", |state, event| {
1233///     component.handle_event(&event.kind, Props { state })
1234/// });
1235/// actions.assert_contains(Action::Select(2));
1236///
1237/// // Snapshot testing
1238/// let output = harness.render_plain(60, 24, |f, area, state| {
1239///     component.render(f, area, Props { state });
1240/// });
1241/// assert!(output.contains("expected text"));
1242/// ```
1243pub struct StoreTestHarness<S, A: Action, E = crate::NoEffect> {
1244    store: crate::Store<S, A, E>,
1245    tx: mpsc::UnboundedSender<A>,
1246    rx: mpsc::UnboundedReceiver<A>,
1247    effects: Vec<E>,
1248    render: Option<RenderHarness>,
1249    default_size: (u16, u16),
1250}
1251
1252impl<S, A: Action, E> StoreTestHarness<S, A, E> {
1253    /// Create a new harness with initial state and reducer.
1254    pub fn new(state: S, reducer: crate::Reducer<S, A, E>) -> Self {
1255        let (tx, rx) = mpsc::unbounded_channel();
1256        Self {
1257            store: crate::Store::new(state, reducer),
1258            tx,
1259            rx,
1260            effects: Vec::new(),
1261            render: None,
1262            default_size: (80, 24),
1263        }
1264    }
1265
1266    /// Set the default terminal size for rendering.
1267    pub fn with_size(mut self, width: u16, height: u16) -> Self {
1268        self.default_size = (width, height);
1269        self
1270    }
1271
1272    // === Store Operations ===
1273
1274    /// Dispatch an action to the store.
1275    ///
1276    /// Returns the full reducer result.
1277    pub fn dispatch(&mut self, action: A) -> crate::ReducerResult<E> {
1278        self.store.dispatch(action)
1279    }
1280
1281    /// Dispatch an action and automatically collect its effects.
1282    ///
1283    /// Returns `true` if state changed. Effects are collected internally
1284    /// and can be retrieved with [`Self::drain_effects`].
1285    pub fn dispatch_collect(&mut self, action: A) -> bool {
1286        let result = self.store.dispatch(action);
1287        self.effects.extend(result.effects);
1288        result.changed
1289    }
1290
1291    /// Dispatch multiple actions in sequence.
1292    ///
1293    /// Returns a vector of booleans indicating which dispatches changed state.
1294    pub fn dispatch_all(&mut self, actions: impl IntoIterator<Item = A>) -> Vec<bool> {
1295        actions
1296            .into_iter()
1297            .map(|a| self.dispatch_collect(a))
1298            .collect()
1299    }
1300
1301    /// Get a reference to the current state.
1302    pub fn state(&self) -> &S {
1303        self.store.state()
1304    }
1305
1306    /// Get a mutable reference to the state for test setup.
1307    pub fn state_mut(&mut self) -> &mut S {
1308        self.store.state_mut()
1309    }
1310
1311    // === Effect Operations ===
1312
1313    /// Drain all collected effects.
1314    pub fn drain_effects(&mut self) -> Vec<E> {
1315        std::mem::take(&mut self.effects)
1316    }
1317
1318    /// Check if any effects were collected.
1319    pub fn has_effects(&self) -> bool {
1320        !self.effects.is_empty()
1321    }
1322
1323    /// Get the number of collected effects.
1324    pub fn effect_count(&self) -> usize {
1325        self.effects.len()
1326    }
1327
1328    // === Action Channel (for async simulation) ===
1329
1330    /// Get a sender clone for simulating async action completions.
1331    pub fn sender(&self) -> mpsc::UnboundedSender<A> {
1332        self.tx.clone()
1333    }
1334
1335    /// Emit an action to the channel (simulates async completion).
1336    pub fn emit(&self, action: A) {
1337        let _ = self.tx.send(action);
1338    }
1339
1340    /// Simulate async action completion (semantic alias for [`Self::emit`]).
1341    pub fn complete_action(&self, action: A) {
1342        self.emit(action);
1343    }
1344
1345    /// Simulate multiple async action completions.
1346    pub fn complete_actions(&self, actions: impl IntoIterator<Item = A>) {
1347        for action in actions {
1348            self.emit(action);
1349        }
1350    }
1351
1352    /// Drain all emitted actions from the channel.
1353    pub fn drain_emitted(&mut self) -> Vec<A> {
1354        let mut actions = Vec::new();
1355        while let Ok(action) = self.rx.try_recv() {
1356            actions.push(action);
1357        }
1358        actions
1359    }
1360
1361    /// Check if any actions were emitted.
1362    pub fn has_emitted(&mut self) -> bool {
1363        !self.drain_emitted().is_empty()
1364    }
1365
1366    /// Process all emitted actions through the store.
1367    ///
1368    /// Drains the channel and dispatches each action to the store.
1369    /// Returns `(changed_count, total_count)`.
1370    pub fn process_emitted(&mut self) -> (usize, usize) {
1371        let actions = self.drain_emitted();
1372        let total = actions.len();
1373        let changed = actions
1374            .into_iter()
1375            .filter(|a| self.dispatch_collect(a.clone()))
1376            .count();
1377        (changed, total)
1378    }
1379
1380    // === Key/Event Handling ===
1381
1382    /// Send a sequence of key events and collect actions from a handler.
1383    ///
1384    /// Parses the space-separated key string and calls the handler for each event,
1385    /// collecting all returned actions.
1386    pub fn send_keys<C, H, I>(&mut self, keys: &str, mut handler: H) -> Vec<A>
1387    where
1388        C: ComponentId,
1389        I: IntoIterator<Item = A>,
1390        H: FnMut(&mut S, Event<C>) -> I,
1391    {
1392        let events = key_events::<C>(keys);
1393        let mut all_actions = Vec::new();
1394        for event in events {
1395            let actions = handler(self.store.state_mut(), event);
1396            all_actions.extend(actions);
1397        }
1398        all_actions
1399    }
1400
1401    /// Send keys and dispatch returned actions to the store.
1402    ///
1403    /// Returns the actions that were dispatched.
1404    pub fn send_keys_dispatch<C, H, I>(&mut self, keys: &str, mut handler: H) -> Vec<A>
1405    where
1406        C: ComponentId,
1407        I: IntoIterator<Item = A>,
1408        H: FnMut(&mut S, Event<C>) -> I,
1409    {
1410        let events = key_events::<C>(keys);
1411        let mut all_actions = Vec::new();
1412        for event in events {
1413            let actions: Vec<A> = handler(self.store.state_mut(), event).into_iter().collect();
1414            for action in actions {
1415                self.dispatch_collect(action.clone());
1416                all_actions.push(action);
1417            }
1418        }
1419        all_actions
1420    }
1421
1422    // === State Assertions ===
1423
1424    /// Assert a condition on the current state.
1425    ///
1426    /// # Panics
1427    ///
1428    /// Panics if the predicate returns `false`.
1429    pub fn assert_state<F>(&self, predicate: F)
1430    where
1431        F: FnOnce(&S) -> bool,
1432    {
1433        assert!(predicate(self.state()), "State assertion failed");
1434    }
1435
1436    /// Assert a condition with a custom message.
1437    pub fn assert_state_msg<F>(&self, predicate: F, msg: &str)
1438    where
1439        F: FnOnce(&S) -> bool,
1440    {
1441        assert!(predicate(self.state()), "{}", msg);
1442    }
1443
1444    // === Render Operations ===
1445
1446    fn ensure_render(&mut self, width: u16, height: u16) {
1447        if self.render.is_none() || self.render.as_ref().map(|r| r.size()) != Some((width, height))
1448        {
1449            self.render = Some(RenderHarness::new(width, height));
1450        }
1451    }
1452
1453    /// Render using the provided function, returns string with ANSI codes.
1454    pub fn render<F>(&mut self, width: u16, height: u16, render_fn: F) -> String
1455    where
1456        F: FnOnce(&mut ratatui::Frame, ratatui::layout::Rect, &S),
1457    {
1458        self.ensure_render(width, height);
1459        let store = &self.store;
1460        let render = self.render.as_mut().unwrap();
1461        render.render_to_string(|frame| {
1462            let area = frame.area();
1463            render_fn(frame, area, store.state());
1464        })
1465    }
1466
1467    /// Render to plain string (no ANSI codes).
1468    pub fn render_plain<F>(&mut self, width: u16, height: u16, render_fn: F) -> String
1469    where
1470        F: FnOnce(&mut ratatui::Frame, ratatui::layout::Rect, &S),
1471    {
1472        self.ensure_render(width, height);
1473        let store = &self.store;
1474        let render = self.render.as_mut().unwrap();
1475        render.render_to_string_plain(|frame| {
1476            let area = frame.area();
1477            render_fn(frame, area, store.state());
1478        })
1479    }
1480
1481    /// Render with default terminal size (set via [`Self::with_size`]).
1482    pub fn render_default<F>(&mut self, render_fn: F) -> String
1483    where
1484        F: FnOnce(&mut ratatui::Frame, ratatui::layout::Rect, &S),
1485    {
1486        let (w, h) = self.default_size;
1487        self.render(w, h, render_fn)
1488    }
1489
1490    /// Render with default size to plain string.
1491    pub fn render_default_plain<F>(&mut self, render_fn: F) -> String
1492    where
1493        F: FnOnce(&mut ratatui::Frame, ratatui::layout::Rect, &S),
1494    {
1495        let (w, h) = self.default_size;
1496        self.render_plain(w, h, render_fn)
1497    }
1498}
1499
1500impl<S: Default, A: Action> Default for StoreTestHarness<S, A> {
1501    fn default() -> Self {
1502        Self::new(S::default(), |_, _| crate::ReducerResult::unchanged())
1503    }
1504}
1505
1506// ============================================================================
1507// EffectAssertions - Fluent assertions for effect vectors
1508// ============================================================================
1509
1510/// Fluent assertions for effect vectors.
1511///
1512/// Method names are prefixed with `effects_` to avoid conflicts with
1513/// [`ActionAssertions`] when both traits are in scope.
1514///
1515/// # Example
1516///
1517/// ```ignore
1518/// use tui_dispatch::testing::EffectAssertions;
1519///
1520/// let effects = harness.drain_effects();
1521/// effects.effects_count(1);
1522/// effects.effects_first_matches(|e| matches!(e, Effect::FetchWeather { .. }));
1523/// ```
1524pub trait EffectAssertions<E> {
1525    /// Assert the effect vector is empty.
1526    fn effects_empty(&self);
1527    /// Assert the effect vector is not empty.
1528    fn effects_not_empty(&self);
1529    /// Assert the effect vector has exactly `n` elements.
1530    fn effects_count(&self, n: usize);
1531    /// Assert any effect matches the predicate.
1532    fn effects_any_matches<F: Fn(&E) -> bool>(&self, f: F);
1533    /// Assert the first effect matches the predicate.
1534    fn effects_first_matches<F: Fn(&E) -> bool>(&self, f: F);
1535    /// Assert all effects match the predicate.
1536    fn effects_all_match<F: Fn(&E) -> bool>(&self, f: F);
1537    /// Assert no effects match the predicate.
1538    fn effects_none_match<F: Fn(&E) -> bool>(&self, f: F);
1539}
1540
1541impl<E: std::fmt::Debug> EffectAssertions<E> for Vec<E> {
1542    fn effects_empty(&self) {
1543        assert!(self.is_empty(), "Expected no effects, got {:?}", self);
1544    }
1545
1546    fn effects_not_empty(&self) {
1547        assert!(!self.is_empty(), "Expected effects, got none");
1548    }
1549
1550    fn effects_count(&self, n: usize) {
1551        assert_eq!(
1552            self.len(),
1553            n,
1554            "Expected {} effects, got {}: {:?}",
1555            n,
1556            self.len(),
1557            self
1558        );
1559    }
1560
1561    fn effects_any_matches<F: Fn(&E) -> bool>(&self, f: F) {
1562        assert!(
1563            self.iter().any(&f),
1564            "No effect matched predicate in {:?}",
1565            self
1566        );
1567    }
1568
1569    fn effects_first_matches<F: Fn(&E) -> bool>(&self, f: F) {
1570        let first = self.first().expect("Expected at least one effect");
1571        assert!(f(first), "First effect {:?} did not match predicate", first);
1572    }
1573
1574    fn effects_all_match<F: Fn(&E) -> bool>(&self, f: F) {
1575        for (i, e) in self.iter().enumerate() {
1576            assert!(f(e), "Effect at index {} did not match: {:?}", i, e);
1577        }
1578    }
1579
1580    fn effects_none_match<F: Fn(&E) -> bool>(&self, f: F) {
1581        for (i, e) in self.iter().enumerate() {
1582            assert!(!f(e), "Effect at index {} unexpectedly matched: {:?}", i, e);
1583        }
1584    }
1585}
1586
1587/// Equality-based assertions for effect vectors.
1588///
1589/// Method names are prefixed with `effects_` to avoid conflicts with
1590/// [`ActionAssertionsEq`] when both traits are in scope.
1591pub trait EffectAssertionsEq<E> {
1592    /// Assert the vector contains the expected effect.
1593    fn effects_contains(&self, expected: E);
1594    /// Assert the first effect equals the expected.
1595    fn effects_first_eq(&self, expected: E);
1596    /// Assert the last effect equals the expected.
1597    fn effects_last_eq(&self, expected: E);
1598}
1599
1600impl<E: PartialEq + std::fmt::Debug> EffectAssertionsEq<E> for Vec<E> {
1601    fn effects_contains(&self, expected: E) {
1602        assert!(
1603            self.contains(&expected),
1604            "Expected to contain {:?}, got {:?}",
1605            expected,
1606            self
1607        );
1608    }
1609
1610    fn effects_first_eq(&self, expected: E) {
1611        let first = self.first().expect("Expected at least one effect");
1612        assert_eq!(first, &expected, "First effect mismatch");
1613    }
1614
1615    fn effects_last_eq(&self, expected: E) {
1616        let last = self.last().expect("Expected at least one effect");
1617        assert_eq!(last, &expected, "Last effect mismatch");
1618    }
1619}
1620
1621// ============================================================================
1622// Time Control (Feature-gated)
1623// ============================================================================
1624
1625/// Time control utilities for testing debounced actions.
1626///
1627/// These functions require the `testing-time` feature and must be used
1628/// within a `#[tokio::test]` context.
1629///
1630/// # Example
1631///
1632/// ```ignore
1633/// use tui_dispatch::testing::{pause_time, advance_time};
1634/// use std::time::Duration;
1635///
1636/// #[tokio::test]
1637/// async fn test_debounce() {
1638///     pause_time();
1639///
1640///     // Simulate typing with debounce
1641///     harness.send_keys("a b c", handler);
1642///
1643///     // Advance past debounce threshold
1644///     advance_time(Duration::from_millis(300)).await;
1645///
1646///     let actions = harness.drain_emitted();
1647///     assert_emitted!(actions, Action::DebouncedSearch { .. });
1648/// }
1649/// ```
1650#[cfg(feature = "testing-time")]
1651pub fn pause_time() {
1652    tokio::time::pause();
1653}
1654
1655/// Resume real-time execution after pausing.
1656#[cfg(feature = "testing-time")]
1657pub fn resume_time() {
1658    tokio::time::resume();
1659}
1660
1661/// Advance the paused clock by the specified duration.
1662///
1663/// Must be called after [`pause_time`].
1664#[cfg(feature = "testing-time")]
1665pub async fn advance_time(duration: std::time::Duration) {
1666    tokio::time::advance(duration).await;
1667}
1668
1669#[cfg(test)]
1670mod tests {
1671    use super::*;
1672
1673    #[test]
1674    fn test_key_simple() {
1675        let k = key("q");
1676        assert_eq!(k.code, KeyCode::Char('q'));
1677        assert_eq!(k.modifiers, KeyModifiers::empty());
1678    }
1679
1680    #[test]
1681    fn test_key_with_ctrl() {
1682        let k = key("ctrl+p");
1683        assert_eq!(k.code, KeyCode::Char('p'));
1684        assert!(k.modifiers.contains(KeyModifiers::CONTROL));
1685    }
1686
1687    #[test]
1688    fn test_key_special() {
1689        let k = key("esc");
1690        assert_eq!(k.code, KeyCode::Esc);
1691
1692        let k = key("enter");
1693        assert_eq!(k.code, KeyCode::Enter);
1694
1695        let k = key("shift+tab");
1696        assert_eq!(k.code, KeyCode::BackTab);
1697    }
1698
1699    #[test]
1700    fn test_char_key() {
1701        let k = char_key('x');
1702        assert_eq!(k.code, KeyCode::Char('x'));
1703        assert_eq!(k.modifiers, KeyModifiers::empty());
1704    }
1705
1706    #[test]
1707    fn test_ctrl_key() {
1708        let k = ctrl_key('c');
1709        assert_eq!(k.code, KeyCode::Char('c'));
1710        assert!(k.modifiers.contains(KeyModifiers::CONTROL));
1711    }
1712
1713    #[derive(Clone, Debug, PartialEq)]
1714    enum TestAction {
1715        Foo,
1716        Bar(i32),
1717    }
1718
1719    impl crate::Action for TestAction {
1720        fn name(&self) -> &'static str {
1721            match self {
1722                TestAction::Foo => "Foo",
1723                TestAction::Bar(_) => "Bar",
1724            }
1725        }
1726    }
1727
1728    #[test]
1729    fn test_harness_emit_and_drain() {
1730        let mut harness = TestHarness::<(), TestAction>::new(());
1731
1732        harness.emit(TestAction::Foo);
1733        harness.emit(TestAction::Bar(42));
1734
1735        let actions = harness.drain_emitted();
1736        assert_eq!(actions.len(), 2);
1737        assert_eq!(actions[0], TestAction::Foo);
1738        assert_eq!(actions[1], TestAction::Bar(42));
1739
1740        // Drain again should be empty
1741        let actions = harness.drain_emitted();
1742        assert!(actions.is_empty());
1743    }
1744
1745    #[test]
1746    fn test_assert_macros() {
1747        let actions = vec![TestAction::Foo, TestAction::Bar(42)];
1748
1749        assert_emitted!(actions, TestAction::Foo);
1750        assert_emitted!(actions, TestAction::Bar(42));
1751        assert_emitted!(actions, TestAction::Bar(_));
1752
1753        assert_not_emitted!(actions, TestAction::Bar(99));
1754
1755        let found = find_emitted!(actions, TestAction::Bar(_));
1756        assert!(found.is_some());
1757
1758        let count = count_emitted!(actions, TestAction::Bar(_));
1759        assert_eq!(count, 1);
1760    }
1761
1762    // ActionAssertions trait tests
1763    #[test]
1764    fn test_action_assertions_first_last() {
1765        let actions = vec![TestAction::Foo, TestAction::Bar(42), TestAction::Bar(99)];
1766
1767        actions.assert_first(TestAction::Foo);
1768        actions.assert_last(TestAction::Bar(99));
1769    }
1770
1771    #[test]
1772    fn test_action_assertions_contains() {
1773        let actions = vec![TestAction::Foo, TestAction::Bar(42)];
1774
1775        actions.assert_contains(TestAction::Foo);
1776        actions.assert_contains(TestAction::Bar(42));
1777        actions.assert_not_contains(TestAction::Bar(99));
1778    }
1779
1780    #[test]
1781    fn test_action_assertions_empty() {
1782        let empty: Vec<TestAction> = vec![];
1783        let non_empty = vec![TestAction::Foo];
1784
1785        empty.assert_empty();
1786        non_empty.assert_not_empty();
1787    }
1788
1789    #[test]
1790    fn test_action_assertions_count() {
1791        let actions = vec![TestAction::Foo, TestAction::Bar(1), TestAction::Bar(2)];
1792        actions.assert_count(3);
1793    }
1794
1795    #[test]
1796    fn test_action_assertions_matches() {
1797        let actions = vec![TestAction::Foo, TestAction::Bar(42), TestAction::Bar(99)];
1798
1799        actions.assert_first_matches(|a| matches!(a, TestAction::Foo));
1800        actions.assert_any_matches(|a| matches!(a, TestAction::Bar(x) if *x > 50));
1801        actions.assert_all_match(|a| matches!(a, TestAction::Foo | TestAction::Bar(_)));
1802        actions.assert_none_match(|a| matches!(a, TestAction::Bar(0)));
1803    }
1804
1805    // key_events / keys tests
1806    #[test]
1807    fn test_keys_multiple() {
1808        let k = keys("a b c");
1809        assert_eq!(k.len(), 3);
1810        assert_eq!(k[0].code, KeyCode::Char('a'));
1811        assert_eq!(k[1].code, KeyCode::Char('b'));
1812        assert_eq!(k[2].code, KeyCode::Char('c'));
1813    }
1814
1815    #[test]
1816    fn test_keys_with_modifiers() {
1817        let k = keys("ctrl+c esc enter");
1818        assert_eq!(k.len(), 3);
1819        assert_eq!(k[0].code, KeyCode::Char('c'));
1820        assert!(k[0].modifiers.contains(KeyModifiers::CONTROL));
1821        assert_eq!(k[1].code, KeyCode::Esc);
1822        assert_eq!(k[2].code, KeyCode::Enter);
1823    }
1824
1825    // RenderHarness tests
1826    #[test]
1827    fn test_render_harness_new() {
1828        let harness = RenderHarness::new(80, 24);
1829        assert_eq!(harness.size(), (80, 24));
1830    }
1831
1832    #[test]
1833    fn test_render_harness_render_plain() {
1834        let mut harness = RenderHarness::new(10, 2);
1835        let output = harness.render_to_string_plain(|frame| {
1836            use ratatui::widgets::Paragraph;
1837            let p = Paragraph::new("Hello");
1838            frame.render_widget(p, frame.area());
1839        });
1840
1841        // Should contain "Hello" followed by spaces to fill the width
1842        assert!(output.starts_with("Hello"));
1843    }
1844
1845    #[test]
1846    fn test_render_harness_resize() {
1847        let mut harness = RenderHarness::new(80, 24);
1848        assert_eq!(harness.size(), (80, 24));
1849
1850        harness.resize(100, 30);
1851        assert_eq!(harness.size(), (100, 30));
1852    }
1853
1854    // complete_action tests
1855    #[test]
1856    fn test_complete_action() {
1857        let mut harness = TestHarness::<(), TestAction>::new(());
1858
1859        harness.complete_action(TestAction::Foo);
1860        harness.complete_actions([TestAction::Bar(1), TestAction::Bar(2)]);
1861
1862        let actions = harness.drain_emitted();
1863        assert_eq!(actions.len(), 3);
1864        actions.assert_first(TestAction::Foo);
1865    }
1866
1867    // assert_state! macro test
1868    #[derive(Default, Debug, PartialEq)]
1869    struct TestState {
1870        count: i32,
1871        name: String,
1872    }
1873
1874    #[test]
1875    fn test_assert_state_macro() {
1876        let harness = TestHarness::<TestState, TestAction>::new(TestState {
1877            count: 42,
1878            name: "test".to_string(),
1879        });
1880
1881        assert_state!(harness, count, 42);
1882        assert_state!(harness, name, "test".to_string());
1883    }
1884}