lv-tui 0.4.0

A reactive TUI framework for Rust
Documentation
/// Integration tests for Timer API (RFC 53).
///
/// Tests set_timer (one-shot), set_interval (periodic), and cancel_timer.
use lv_tui::prelude::*;
use lv_tui::Component;

// ── set_timer (one-shot) ───────────────────────────────────────

#[derive(Component)]
struct OneShot {
    #[reactive(paint, copy)]
    count: i32,
}

impl OneShot {
    fn new() -> Self { Self { count: 0 } }
}

impl Component for OneShot {
    fn render(&self, cx: &mut RenderCx) {
        cx.line(&format!("count: {}", self.get_count()));
    }

    fn mount(&mut self, cx: &mut EventCx) {
        cx.set_timer(5); // 5ms one-shot
    }

    fn event(&mut self, event: &Event, cx: &mut EventCx) {
        if let Event::Timer(_) = event {
            self.set_count(self.get_count() + 1, cx);
        }
        if event.is_key(Key::Char('q')) {
            cx.quit();
        }
    }
}

#[test]
fn set_timer_fires_once() {
    let mut pilot = Pilot::new(OneShot::new(), 20, 3);

    // Run until timer fires (max 100 steps, each step polls with tick_rate)
    let fired = pilot.run_until(50, |buf| {
        buf.cells.iter().any(|c| c.symbol == "1")
    });
    assert!(fired.unwrap(), "timer should have fired");

    // After many more steps, count should still be 1 (no repeat)
    for _ in 0..20 {
        let _ = pilot.send_event(Event::Tick);
    }
    assert!(pilot.frame().cells.iter().any(|c| c.symbol == "1"),
        "one-shot timer should not fire again");
    assert!(!pilot.frame().cells.iter().any(|c| c.symbol == "2"),
        "one-shot timer should fire exactly once");
}

// ── set_interval (periodic) ────────────────────────────────────

#[derive(Component)]
struct Periodic {
    #[reactive(paint, copy)]
    ticks: i32,
    timer_id: Option<u64>,
}

impl Periodic {
    fn new() -> Self { Self { ticks: 0, timer_id: None } }
}

impl Component for Periodic {
    fn render(&self, cx: &mut RenderCx) {
        cx.line(&format!("ticks: {}", self.get_ticks()));
    }

    fn mount(&mut self, cx: &mut EventCx) {
        // Every 5ms interval
        self.timer_id = Some(cx.set_interval(5));
    }

    fn event(&mut self, event: &Event, cx: &mut EventCx) {
        if let Event::Timer(_) = event {
            self.set_ticks(self.get_ticks() + 1, cx);
        }
        if event.is_key(Key::Char('q')) {
            cx.quit();
        }
    }
}

#[test]
fn set_interval_fires_multiple_times() {
    let mut pilot = Pilot::new(Periodic::new(), 20, 3);

    // Wait for at least 3 timer fires
    let fired = pilot.run_until(100, |buf| {
        buf.cells.iter().any(|c| c.symbol == "3")
    });
    assert!(fired.unwrap(), "interval timer should fire at least 3 times");
}

// ── cancel_timer ───────────────────────────────────────────────

#[derive(Component)]
struct CancelTimer {
    #[reactive(paint, copy)]
    count: i32,
    timer_id: Option<u64>,
}

impl CancelTimer {
    fn new() -> Self { Self { count: 0, timer_id: None } }
}

impl Component for CancelTimer {
    fn render(&self, cx: &mut RenderCx) {
        cx.line(&format!("count: {}", self.get_count()));
    }

    fn mount(&mut self, cx: &mut EventCx) {
        self.timer_id = Some(cx.set_interval(5));
    }

    fn event(&mut self, event: &Event, cx: &mut EventCx) {
        match event {
            Event::Timer(_) => {
                self.set_count(self.get_count() + 1, cx);
                // Cancel after first fire
                if self.get_count() == 1 {
                    if let Some(id) = self.timer_id.take() {
                        cx.cancel_timer(id);
                    }
                }
            }
            _ => {}
        }
        if event.is_key(Key::Char('q')) {
            cx.quit();
        }
    }
}

#[test]
fn cancel_timer_stops_firing() {
    let mut pilot = Pilot::new(CancelTimer::new(), 20, 3);

    // Wait for first timer fire
    pilot.run_until(50, |buf| {
        buf.cells.iter().any(|c| c.symbol == "1")
    }).unwrap();

    // Sleep enough that more would have fired if not cancelled
    std::thread::sleep(std::time::Duration::from_millis(30));

    // Pump a few more steps
    for _ in 0..10 {
        let _ = pilot.send_event(Event::Tick);
    }

    // Count should still be 1 (timer was cancelled)
    assert!(!pilot.frame().cells.iter().any(|c| c.symbol == "2"),
        "cancelled timer should not fire again");
}

// ── Multiple timers on same component ──────────────────────────

#[derive(Component)]
struct MultiTimer {
    #[reactive(paint, copy)]
    a: i32,
    #[reactive(paint, copy)]
    b: i32,
}

impl MultiTimer {
    fn new() -> Self { Self { a: 0, b: 0 } }
}

impl Component for MultiTimer {
    fn render(&self, cx: &mut RenderCx) {
        cx.line(&format!("a:{} b:{}", self.get_a(), self.get_b()));
    }

    fn mount(&mut self, cx: &mut EventCx) {
        cx.set_interval(5);  // fast
        cx.set_interval(15); // slow
    }

    fn event(&mut self, event: &Event, cx: &mut EventCx) {
        if let Event::Timer(_) = event {
            // Both timers share the same handler — just count up
            self.set_a(self.get_a() + 1, cx);
            if self.get_a() >= 3 {
                cx.quit();
            }
        }
    }
}

#[test]
fn multiple_timers_on_same_component() {
    let mut pilot = Pilot::new(MultiTimer::new(), 20, 3);
    let quit = pilot.run_until_quit(100).unwrap();
    assert!(quit, "timers should fire and app should quit");
    assert!(pilot.frame().cells.iter().any(|c| c.symbol != " "),
        "should have rendered timer results");
}

// ── Timer cleanup on unmount ───────────────────────────────────

#[derive(Component)]
struct UnmountCleanup {
    #[reactive(paint, copy)]
    count: i32,
}

impl UnmountCleanup {
    fn new() -> Self { Self { count: 0 } }
}

impl Component for UnmountCleanup {
    fn render(&self, cx: &mut RenderCx) {
        cx.line(&format!("count: {}", self.get_count()));
    }

    fn mount(&mut self, cx: &mut EventCx) {
        cx.set_interval(5);
    }

    fn event(&mut self, event: &Event, cx: &mut EventCx) {
        if let Event::Timer(_) = event {
            self.set_count(self.get_count() + 1, cx);
        }
        if event.is_key(Key::Char('q')) {
            cx.quit();
        }
    }
}

#[test]
fn timer_unmount_cleanup() {
    let mut pilot = Pilot::new(UnmountCleanup::new(), 20, 3);

    // Wait for at least one fire
    pilot.run_until(50, |buf| {
        buf.cells.iter().any(|c| c.symbol == "1")
    }).unwrap();

    // Quit the app (which triggers unmount)
    pilot.press(Key::Char('q')).unwrap();

    // After quit, no more timers should fire
    assert!(pilot.has_quit());
}