Skip to main content

egui_cha/
testing.rs

1//! Testing utilities for egui-cha applications
2//!
3//! # Example
4//! ```ignore
5//! use egui_cha::testing::TestRunner;
6//!
7//! #[test]
8//! fn test_counter_flow() {
9//!     let mut runner = TestRunner::<CounterApp>::new();
10//!
11//!     runner
12//!         .send(Msg::Increment)
13//!         .send(Msg::Increment)
14//!         .send(Msg::Decrement);
15//!
16//!     assert_eq!(runner.model().count, 1);
17//! }
18//! ```
19
20use crate::helpers::Clock;
21use crate::{App, Cmd};
22use std::cell::Cell;
23use std::future::Future;
24use std::pin::Pin;
25use std::rc::Rc;
26use std::time::Duration;
27
28/// A boxed future for async tasks
29type BoxFuture<Msg> = Pin<Box<dyn Future<Output = Msg> + Send + 'static>>;
30
31// ============================================
32// FakeClock for testing time-dependent code
33// ============================================
34
35/// A fake clock for testing time-dependent code
36///
37/// Allows manual control of time progression, enabling fast and
38/// deterministic tests for `Debouncer`, `Throttler`, etc.
39///
40/// # Example
41/// ```ignore
42/// use egui_cha::testing::FakeClock;
43/// use egui_cha::helpers::Debouncer;
44/// use std::time::Duration;
45///
46/// let clock = FakeClock::new();
47/// let mut debouncer = Debouncer::with_clock(clock.clone());
48///
49/// debouncer.trigger(Duration::from_millis(500), Msg::Search);
50///
51/// // Time hasn't passed yet
52/// assert!(!debouncer.should_fire());
53///
54/// // Advance time past the debounce delay
55/// clock.advance(Duration::from_millis(600));
56/// assert!(debouncer.should_fire());
57/// ```
58#[derive(Clone)]
59pub struct FakeClock {
60    current: Rc<Cell<Duration>>,
61}
62
63impl FakeClock {
64    /// Create a new fake clock starting at time zero
65    pub fn new() -> Self {
66        Self {
67            current: Rc::new(Cell::new(Duration::ZERO)),
68        }
69    }
70
71    /// Advance the clock by the specified duration
72    pub fn advance(&self, duration: Duration) {
73        self.current.set(self.current.get() + duration);
74    }
75
76    /// Set the clock to a specific time
77    pub fn set(&self, time: Duration) {
78        self.current.set(time);
79    }
80
81    /// Get the current time
82    pub fn get(&self) -> Duration {
83        self.current.get()
84    }
85
86    /// Reset the clock to time zero
87    pub fn reset(&self) {
88        self.current.set(Duration::ZERO);
89    }
90}
91
92impl Default for FakeClock {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98impl Clock for FakeClock {
99    fn now(&self) -> Duration {
100        self.current.get()
101    }
102}
103
104/// A test runner for TEA applications
105///
106/// Provides a convenient way to test update logic without running the UI.
107pub struct TestRunner<A: App> {
108    model: A::Model,
109    commands: Vec<CmdRecord<A::Msg>>,
110    pending_tasks: Vec<BoxFuture<A::Msg>>,
111}
112
113/// Record of a command that was returned from update
114#[derive(Debug)]
115pub enum CmdRecord<Msg> {
116    None,
117    Task,
118    Msg(Msg),
119    Batch(usize),
120}
121
122impl<A: App> TestRunner<A> {
123    /// Create a new test runner with initial model
124    pub fn new() -> Self {
125        let (model, init_cmd) = A::init();
126        let mut runner = Self {
127            model,
128            commands: Vec::new(),
129            pending_tasks: Vec::new(),
130        };
131        runner.record_cmd(init_cmd);
132        runner
133    }
134
135    /// Create a test runner with a custom initial model
136    pub fn with_model(model: A::Model) -> Self {
137        Self {
138            model,
139            commands: Vec::new(),
140            pending_tasks: Vec::new(),
141        }
142    }
143
144    /// Send a message and process the update
145    pub fn send(&mut self, msg: A::Msg) -> &mut Self {
146        let cmd = A::update(&mut self.model, msg);
147        self.record_cmd(cmd);
148        self
149    }
150
151    /// Send multiple messages in sequence
152    pub fn send_all(&mut self, msgs: impl IntoIterator<Item = A::Msg>) -> &mut Self {
153        for msg in msgs {
154            self.send(msg);
155        }
156        self
157    }
158
159    /// Get a reference to the current model
160    pub fn model(&self) -> &A::Model {
161        &self.model
162    }
163
164    /// Get a mutable reference to the model (for setup)
165    pub fn model_mut(&mut self) -> &mut A::Model {
166        &mut self.model
167    }
168
169    /// Get the last command record
170    pub fn last_cmd(&self) -> Option<&CmdRecord<A::Msg>> {
171        self.commands.last()
172    }
173
174    /// Get all command records
175    pub fn commands(&self) -> &[CmdRecord<A::Msg>] {
176        &self.commands
177    }
178
179    /// Clear command history
180    pub fn clear_commands(&mut self) -> &mut Self {
181        self.commands.clear();
182        self
183    }
184
185    /// Check if the last command was Cmd::None
186    pub fn last_was_none(&self) -> bool {
187        matches!(self.last_cmd(), Some(CmdRecord::None))
188    }
189
190    /// Check if the last command was Cmd::Task
191    pub fn last_was_task(&self) -> bool {
192        matches!(self.last_cmd(), Some(CmdRecord::Task))
193    }
194
195    /// Check if the last command was Cmd::Msg
196    pub fn last_was_msg(&self) -> bool {
197        matches!(self.last_cmd(), Some(CmdRecord::Msg(_)))
198    }
199
200    /// Get a string describing the kind of the last command (for error messages)
201    fn last_cmd_kind(&self) -> &'static str {
202        match self.last_cmd() {
203            Some(CmdRecord::None) => "None",
204            Some(CmdRecord::Task) => "Task",
205            Some(CmdRecord::Msg(_)) => "Msg",
206            Some(CmdRecord::Batch(_)) => "Batch",
207            None => "<no command>",
208        }
209    }
210
211    fn record_cmd(&mut self, cmd: Cmd<A::Msg>) {
212        let record = match cmd {
213            Cmd::None => CmdRecord::None,
214            Cmd::Task(future) => {
215                self.pending_tasks.push(future);
216                CmdRecord::Task
217            }
218            Cmd::Msg(msg) => CmdRecord::Msg(msg),
219            Cmd::Batch(cmds) => {
220                let len = cmds.len();
221                // Extract tasks from batch
222                for cmd in cmds {
223                    self.extract_tasks(cmd);
224                }
225                CmdRecord::Batch(len)
226            }
227        };
228        self.commands.push(record);
229    }
230
231    /// Extract tasks from a command (recursively for batches)
232    fn extract_tasks(&mut self, cmd: Cmd<A::Msg>) {
233        match cmd {
234            Cmd::None | Cmd::Msg(_) => {}
235            Cmd::Task(future) => {
236                self.pending_tasks.push(future);
237            }
238            Cmd::Batch(cmds) => {
239                for cmd in cmds {
240                    self.extract_tasks(cmd);
241                }
242            }
243        }
244    }
245
246    // ========================================
247    // Async task processing
248    // ========================================
249
250    /// Get the number of pending async tasks
251    pub fn pending_task_count(&self) -> usize {
252        self.pending_tasks.len()
253    }
254
255    /// Check if there are any pending async tasks
256    pub fn has_pending_tasks(&self) -> bool {
257        !self.pending_tasks.is_empty()
258    }
259
260    /// Process one pending async task
261    ///
262    /// Executes the first pending task, awaits its result, and sends
263    /// the resulting message through update.
264    ///
265    /// Returns `true` if a task was processed, `false` if no tasks were pending.
266    ///
267    /// # Example
268    /// ```ignore
269    /// runner.send(Msg::FetchData);
270    /// assert!(runner.has_pending_tasks());
271    ///
272    /// runner.process_task().await;
273    /// assert!(!runner.has_pending_tasks());
274    /// ```
275    pub async fn process_task(&mut self) -> bool {
276        if let Some(task) = self.pending_tasks.pop() {
277            let msg = task.await;
278            self.send(msg);
279            true
280        } else {
281            false
282        }
283    }
284
285    /// Process all pending async tasks
286    ///
287    /// Processes tasks until none remain. Note that processing a task
288    /// may add new tasks (if the resulting message produces new Cmd::Task),
289    /// so this processes until the queue is fully drained.
290    ///
291    /// # Example
292    /// ```ignore
293    /// runner.send(Msg::FetchData);
294    /// runner.process_tasks().await;
295    /// // All tasks completed, results sent through update
296    /// ```
297    pub async fn process_tasks(&mut self) -> &mut Self {
298        while let Some(task) = self.pending_tasks.pop() {
299            let msg = task.await;
300            self.send(msg);
301        }
302        self
303    }
304
305    /// Process exactly N pending tasks
306    ///
307    /// Useful when you want to control the order of task execution
308    /// or test intermediate states.
309    ///
310    /// # Panics
311    /// Panics if there are fewer than N pending tasks.
312    pub async fn process_n_tasks(&mut self, n: usize) -> &mut Self {
313        for i in 0..n {
314            assert!(
315                !self.pending_tasks.is_empty(),
316                "process_n_tasks: expected {} tasks but only {} were available",
317                n,
318                i
319            );
320            let task = self.pending_tasks.remove(0);
321            let msg = task.await;
322            self.send(msg);
323        }
324        self
325    }
326
327    // ========================================
328    // Expect系アサーションメソッド
329    // ========================================
330
331    /// Assert that the model satisfies a predicate
332    ///
333    /// # Example
334    /// ```ignore
335    /// runner
336    ///     .send(Msg::Inc)
337    ///     .expect_model(|m| m.count == 1)
338    ///     .send(Msg::Inc)
339    ///     .expect_model(|m| m.count == 2);
340    /// ```
341    ///
342    /// # Panics
343    /// Panics if the predicate returns false
344    pub fn expect_model(&mut self, predicate: impl FnOnce(&A::Model) -> bool) -> &mut Self {
345        assert!(
346            predicate(&self.model),
347            "expect_model: predicate returned false"
348        );
349        self
350    }
351
352    /// Assert that the model satisfies a predicate with custom message
353    ///
354    /// # Panics
355    /// Panics with the provided message if the predicate returns false
356    pub fn expect_model_msg(
357        &mut self,
358        predicate: impl FnOnce(&A::Model) -> bool,
359        msg: &str,
360    ) -> &mut Self {
361        assert!(predicate(&self.model), "expect_model: {}", msg);
362        self
363    }
364
365    /// Assert that the last command was `Cmd::None`
366    ///
367    /// # Example
368    /// ```ignore
369    /// runner
370    ///     .send(Msg::SetValue(42))
371    ///     .expect_cmd_none();
372    /// ```
373    ///
374    /// # Panics
375    /// Panics if the last command was not `Cmd::None`
376    pub fn expect_cmd_none(&mut self) -> &mut Self {
377        assert!(
378            self.last_was_none(),
379            "expect_cmd_none: last command was {}, expected None",
380            self.last_cmd_kind()
381        );
382        self
383    }
384
385    /// Assert that the last command was `Cmd::Task`
386    ///
387    /// # Example
388    /// ```ignore
389    /// runner
390    ///     .send(Msg::FetchData)
391    ///     .expect_cmd_task();
392    /// ```
393    ///
394    /// # Panics
395    /// Panics if the last command was not `Cmd::Task`
396    pub fn expect_cmd_task(&mut self) -> &mut Self {
397        assert!(
398            self.last_was_task(),
399            "expect_cmd_task: last command was {}, expected Task",
400            self.last_cmd_kind()
401        );
402        self
403    }
404
405    /// Assert that the last command was `Cmd::Msg`
406    ///
407    /// # Example
408    /// ```ignore
409    /// runner
410    ///     .send(Msg::TriggerDelayed)
411    ///     .expect_cmd_msg();
412    /// ```
413    ///
414    /// # Panics
415    /// Panics if the last command was not `Cmd::Msg`
416    pub fn expect_cmd_msg(&mut self) -> &mut Self {
417        assert!(
418            self.last_was_msg(),
419            "expect_cmd_msg: last command was {}, expected Msg",
420            self.last_cmd_kind()
421        );
422        self
423    }
424
425    /// Assert that the last command was `Cmd::Msg` and verify its content
426    ///
427    /// # Example
428    /// ```ignore
429    /// runner
430    ///     .send(Msg::TriggerDelayed)
431    ///     .expect_cmd_msg_eq(Msg::Inc);
432    /// ```
433    ///
434    /// # Panics
435    /// Panics if the last command was not `Cmd::Msg` or the message doesn't match
436    pub fn expect_cmd_msg_eq(&mut self, expected: A::Msg) -> &mut Self
437    where
438        A::Msg: PartialEq + std::fmt::Debug,
439    {
440        match self.last_cmd() {
441            Some(CmdRecord::Msg(msg)) => {
442                assert_eq!(msg, &expected, "expect_cmd_msg_eq: message mismatch");
443            }
444            _ => {
445                panic!(
446                    "expect_cmd_msg_eq: last command was {}, expected Msg({:?})",
447                    self.last_cmd_kind(),
448                    expected
449                );
450            }
451        }
452        self
453    }
454
455    /// Assert that the last command was `Cmd::Batch`
456    ///
457    /// # Example
458    /// ```ignore
459    /// runner
460    ///     .send(Msg::MultiAction)
461    ///     .expect_cmd_batch();
462    /// ```
463    ///
464    /// # Panics
465    /// Panics if the last command was not `Cmd::Batch`
466    pub fn expect_cmd_batch(&mut self) -> &mut Self {
467        assert!(
468            matches!(self.last_cmd(), Some(CmdRecord::Batch(_))),
469            "expect_cmd_batch: last command was {}, expected Batch",
470            self.last_cmd_kind()
471        );
472        self
473    }
474
475    /// Assert that the last command was `Cmd::Batch` with expected size
476    ///
477    /// # Example
478    /// ```ignore
479    /// runner
480    ///     .send(Msg::MultiAction)
481    ///     .expect_cmd_batch_size(3);
482    /// ```
483    ///
484    /// # Panics
485    /// Panics if the last command was not `Cmd::Batch` or size doesn't match
486    pub fn expect_cmd_batch_size(&mut self, expected_size: usize) -> &mut Self {
487        match self.last_cmd() {
488            Some(CmdRecord::Batch(size)) => {
489                assert_eq!(
490                    *size, expected_size,
491                    "expect_cmd_batch_size: batch size mismatch (got {}, expected {})",
492                    size, expected_size
493                );
494            }
495            _ => {
496                panic!(
497                    "expect_cmd_batch_size: last command was {}, expected Batch({})",
498                    self.last_cmd_kind(),
499                    expected_size
500                );
501            }
502        }
503        self
504    }
505}
506
507impl<A: App> Default for TestRunner<A> {
508    fn default() -> Self {
509        Self::new()
510    }
511}
512
513/// Extension trait for asserting on model state
514pub trait ModelAssert<T> {
515    /// Assert with a predicate
516    fn assert_that(&self, predicate: impl FnOnce(&T) -> bool, msg: &str);
517}
518
519impl<A: App> ModelAssert<A::Model> for TestRunner<A> {
520    fn assert_that(&self, predicate: impl FnOnce(&A::Model) -> bool, msg: &str) {
521        assert!(predicate(&self.model), "{}", msg);
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528
529    // Simple test app for testing the test runner
530    struct TestApp;
531
532    #[derive(Default)]
533    struct TestModel {
534        value: i32,
535    }
536
537    #[derive(Clone, Debug, PartialEq)]
538    enum TestMsg {
539        Inc,
540        Dec,
541        Set(i32),
542        Delayed,
543        MultiBatch,
544        AsyncFetch,
545        FetchResult(i32),
546    }
547
548    impl App for TestApp {
549        type Model = TestModel;
550        type Msg = TestMsg;
551
552        fn init() -> (Self::Model, Cmd<Self::Msg>) {
553            (TestModel::default(), Cmd::none())
554        }
555
556        fn update(model: &mut Self::Model, msg: Self::Msg) -> Cmd<Self::Msg> {
557            match msg {
558                TestMsg::Inc => model.value += 1,
559                TestMsg::Dec => model.value -= 1,
560                TestMsg::Set(v) => model.value = v,
561                TestMsg::Delayed => {
562                    return Cmd::msg(TestMsg::Inc);
563                }
564                TestMsg::MultiBatch => {
565                    return Cmd::batch([Cmd::msg(TestMsg::Inc), Cmd::msg(TestMsg::Inc)]);
566                }
567                TestMsg::AsyncFetch => {
568                    return Cmd::task(async { TestMsg::FetchResult(42) });
569                }
570                TestMsg::FetchResult(v) => model.value = v,
571            }
572            Cmd::none()
573        }
574
575        fn view(_model: &Self::Model, _ctx: &mut crate::ViewCtx<Self::Msg>) {
576            // No-op for testing
577        }
578    }
579
580    #[test]
581    fn test_runner_basic() {
582        let mut runner = TestRunner::<TestApp>::new();
583
584        runner.send(TestMsg::Inc);
585        assert_eq!(runner.model().value, 1);
586
587        runner.send(TestMsg::Inc).send(TestMsg::Inc);
588        assert_eq!(runner.model().value, 3);
589
590        runner.send(TestMsg::Dec);
591        assert_eq!(runner.model().value, 2);
592    }
593
594    #[test]
595    fn test_runner_cmd_tracking() {
596        let mut runner = TestRunner::<TestApp>::new();
597
598        runner.send(TestMsg::Inc);
599        assert!(runner.last_was_none());
600
601        runner.send(TestMsg::Delayed);
602        assert!(runner.last_was_msg());
603    }
604
605    #[test]
606    fn test_runner_send_all() {
607        let mut runner = TestRunner::<TestApp>::new();
608
609        runner.send_all([TestMsg::Inc, TestMsg::Inc, TestMsg::Inc]);
610        assert_eq!(runner.model().value, 3);
611    }
612
613    #[test]
614    fn test_expect_model() {
615        let mut runner = TestRunner::<TestApp>::new();
616
617        runner
618            .send(TestMsg::Inc)
619            .expect_model(|m| m.value == 1)
620            .send(TestMsg::Inc)
621            .expect_model(|m| m.value == 2)
622            .send(TestMsg::Set(100))
623            .expect_model(|m| m.value == 100);
624    }
625
626    #[test]
627    fn test_expect_cmd_none() {
628        let mut runner = TestRunner::<TestApp>::new();
629
630        runner.send(TestMsg::Inc).expect_cmd_none();
631    }
632
633    #[test]
634    fn test_expect_cmd_msg() {
635        let mut runner = TestRunner::<TestApp>::new();
636
637        runner.send(TestMsg::Delayed).expect_cmd_msg();
638    }
639
640    #[test]
641    fn test_expect_cmd_msg_eq() {
642        let mut runner = TestRunner::<TestApp>::new();
643
644        runner
645            .send(TestMsg::Delayed)
646            .expect_cmd_msg_eq(TestMsg::Inc);
647    }
648
649    #[test]
650    fn test_expect_cmd_batch() {
651        let mut runner = TestRunner::<TestApp>::new();
652
653        runner
654            .send(TestMsg::MultiBatch)
655            .expect_cmd_batch()
656            .expect_cmd_batch_size(2);
657    }
658
659    #[test]
660    fn test_expect_chaining() {
661        // Fluent API chaining test
662        let mut runner = TestRunner::<TestApp>::new();
663
664        runner
665            .send(TestMsg::Inc)
666            .expect_model(|m| m.value == 1)
667            .expect_cmd_none()
668            .send(TestMsg::Inc)
669            .expect_model(|m| m.value == 2)
670            .expect_cmd_none()
671            .send(TestMsg::Delayed)
672            .expect_model(|m| m.value == 2) // Delayed doesn't change value directly
673            .expect_cmd_msg_eq(TestMsg::Inc);
674    }
675
676    #[cfg(feature = "tokio")]
677    fn block_on<F: std::future::Future>(f: F) -> F::Output {
678        tokio::runtime::Builder::new_current_thread()
679            .enable_all()
680            .build()
681            .unwrap()
682            .block_on(f)
683    }
684
685    #[test]
686    #[cfg(feature = "tokio")]
687    fn test_process_task() {
688        block_on(async {
689            let mut runner = TestRunner::<TestApp>::new();
690
691            // Send a message that produces an async task
692            runner.send(TestMsg::AsyncFetch);
693            assert!(runner.last_was_task());
694            assert!(runner.has_pending_tasks());
695            assert_eq!(runner.pending_task_count(), 1);
696
697            // Model hasn't changed yet
698            assert_eq!(runner.model().value, 0);
699
700            // Process the async task
701            runner.process_task().await;
702
703            // Task completed, result was sent through update
704            assert!(!runner.has_pending_tasks());
705            assert_eq!(runner.model().value, 42);
706        });
707    }
708
709    #[test]
710    #[cfg(feature = "tokio")]
711    fn test_process_tasks() {
712        block_on(async {
713            let mut runner = TestRunner::<TestApp>::new();
714
715            // Queue multiple async tasks
716            runner.send(TestMsg::AsyncFetch);
717            runner.send(TestMsg::AsyncFetch);
718            assert_eq!(runner.pending_task_count(), 2);
719
720            // Process all tasks
721            runner.process_tasks().await;
722
723            // All tasks completed
724            assert!(!runner.has_pending_tasks());
725            // Last FetchResult(42) sets value to 42
726            assert_eq!(runner.model().value, 42);
727        });
728    }
729
730    #[test]
731    #[cfg(feature = "tokio")]
732    fn test_async_expect_chaining() {
733        block_on(async {
734            let mut runner = TestRunner::<TestApp>::new();
735
736            runner
737                .send(TestMsg::Inc)
738                .expect_model(|m| m.value == 1)
739                .expect_cmd_none()
740                .send(TestMsg::AsyncFetch)
741                .expect_cmd_task();
742
743            // Process async task
744            runner.process_tasks().await;
745
746            runner.expect_model(|m| m.value == 42);
747        });
748    }
749
750    // ========================================
751    // FakeClock tests
752    // ========================================
753
754    #[test]
755    fn test_fake_clock_basic() {
756        let clock = super::FakeClock::new();
757
758        assert_eq!(clock.get(), Duration::ZERO);
759
760        clock.advance(Duration::from_millis(100));
761        assert_eq!(clock.get(), Duration::from_millis(100));
762
763        clock.advance(Duration::from_millis(50));
764        assert_eq!(clock.get(), Duration::from_millis(150));
765    }
766
767    #[test]
768    fn test_fake_clock_set_and_reset() {
769        let clock = super::FakeClock::new();
770
771        clock.set(Duration::from_secs(10));
772        assert_eq!(clock.get(), Duration::from_secs(10));
773
774        clock.reset();
775        assert_eq!(clock.get(), Duration::ZERO);
776    }
777
778    #[test]
779    fn test_fake_clock_shared() {
780        let clock1 = super::FakeClock::new();
781        let clock2 = clock1.clone();
782
783        clock1.advance(Duration::from_millis(100));
784
785        // Both clocks share the same time
786        assert_eq!(clock2.get(), Duration::from_millis(100));
787    }
788
789    #[test]
790    #[cfg(feature = "tokio")]
791    fn test_debouncer_with_fake_clock() {
792        use crate::helpers::DebouncerWithClock;
793
794        let clock = super::FakeClock::new();
795        let mut debouncer = DebouncerWithClock::new(clock.clone());
796
797        // Trigger with 500ms delay
798        let _cmd = debouncer.trigger(Duration::from_millis(500), ());
799        assert!(debouncer.is_pending());
800        assert!(!debouncer.should_fire()); // Not yet
801
802        // Advance 300ms - still not ready
803        clock.advance(Duration::from_millis(300));
804        assert!(!debouncer.should_fire());
805
806        // Advance another 100ms - still not ready (400ms total)
807        clock.advance(Duration::from_millis(100));
808        assert!(!debouncer.should_fire());
809
810        // Advance 150ms - now ready (550ms total)
811        clock.advance(Duration::from_millis(150));
812        assert!(debouncer.should_fire());
813        assert!(!debouncer.is_pending());
814    }
815
816    #[test]
817    #[cfg(feature = "tokio")]
818    fn test_debouncer_reset_with_fake_clock() {
819        use crate::helpers::DebouncerWithClock;
820
821        let clock = super::FakeClock::new();
822        let mut debouncer = DebouncerWithClock::new(clock.clone());
823
824        // First trigger
825        let _cmd = debouncer.trigger(Duration::from_millis(500), ());
826
827        // Advance 300ms
828        clock.advance(Duration::from_millis(300));
829        assert!(!debouncer.should_fire());
830
831        // Trigger again (resets timer)
832        let _cmd = debouncer.trigger(Duration::from_millis(500), ());
833
834        // Advance 300ms from reset point - not yet (timer was reset)
835        clock.advance(Duration::from_millis(300));
836        assert!(!debouncer.should_fire());
837
838        // Advance 250ms more - now ready
839        clock.advance(Duration::from_millis(250));
840        assert!(debouncer.should_fire());
841    }
842
843    // Non-tokio tests using mark_trigger
844    #[test]
845    fn test_debouncer_with_fake_clock_mark_trigger() {
846        use crate::helpers::DebouncerWithClock;
847
848        let clock = super::FakeClock::new();
849        let mut debouncer = DebouncerWithClock::new(clock.clone());
850
851        // mark_trigger with 500ms delay
852        debouncer.mark_trigger(Duration::from_millis(500));
853        assert!(debouncer.is_pending());
854        assert!(!debouncer.should_fire());
855
856        // Advance 550ms - now ready
857        clock.advance(Duration::from_millis(550));
858        assert!(debouncer.should_fire());
859        assert!(!debouncer.is_pending());
860    }
861
862    #[test]
863    fn test_throttler_with_fake_clock() {
864        use crate::helpers::ThrottlerWithClock;
865
866        let clock = super::FakeClock::new();
867        let mut throttler = ThrottlerWithClock::new(clock.clone());
868        let interval = Duration::from_millis(100);
869
870        // First call executes
871        let cmd1 = throttler.run(interval, || Cmd::Msg(1));
872        assert!(cmd1.is_msg());
873
874        // Immediate second call is throttled
875        let cmd2 = throttler.run(interval, || Cmd::Msg(2));
876        assert!(cmd2.is_none());
877
878        // Advance 50ms - still throttled
879        clock.advance(Duration::from_millis(50));
880        let cmd3 = throttler.run(interval, || Cmd::Msg(3));
881        assert!(cmd3.is_none());
882
883        // Advance 60ms more (110ms total) - now executes
884        clock.advance(Duration::from_millis(60));
885        let cmd4 = throttler.run(interval, || Cmd::Msg(4));
886        assert!(cmd4.is_msg());
887    }
888
889    #[test]
890    fn test_throttler_time_remaining_with_fake_clock() {
891        use crate::helpers::ThrottlerWithClock;
892
893        let clock = super::FakeClock::new();
894        let mut throttler = ThrottlerWithClock::new(clock.clone());
895        let interval = Duration::from_millis(100);
896
897        // Before first run
898        assert!(throttler.time_remaining(interval).is_none());
899
900        // After first run
901        let _ = throttler.run(interval, || Cmd::Msg(1));
902        let remaining = throttler.time_remaining(interval);
903        assert_eq!(remaining, Some(Duration::from_millis(100)));
904
905        // After 30ms
906        clock.advance(Duration::from_millis(30));
907        let remaining = throttler.time_remaining(interval);
908        assert_eq!(remaining, Some(Duration::from_millis(70)));
909
910        // After interval expires
911        clock.advance(Duration::from_millis(80));
912        assert!(throttler.time_remaining(interval).is_none());
913    }
914}