lv-tui 0.4.0

A reactive TUI framework for Rust
Documentation
/// Integration tests for Worker system (RFC 56).
use lv_tui::prelude::*;
use lv_tui::event::WorkerId;
use lv_tui::Component;

// ── Basic spawn (backward compat) ──────────────────────────────

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

impl SimpleWorker {
    fn new() -> Self { Self { status: 0 } }
}

impl Component for SimpleWorker {
    fn render(&self, cx: &mut RenderCx) {
        cx.line(&format!("status: {}", self.get_status()));
    }
    fn mount(&mut self, cx: &mut EventCx) {
        self.set_status(1, cx);
        cx.spawn(move || {
            std::thread::sleep(std::time::Duration::from_millis(10));
            "done".to_string()
        });
    }
    fn event(&mut self, event: &Event, cx: &mut EventCx) {
        if let Event::TaskComplete(ref result) = event {
            if result == "done" {
                self.set_status(2, cx);
            }
        }
    }
}

#[test]
fn spawn_completes_and_delivers_result() {
    let mut pilot = Pilot::new(SimpleWorker::new(), 20, 3);
    let done = pilot.run_until(100, |buf| {
        buf.cells.iter().any(|c| c.symbol == "2")
    }).unwrap();
    assert!(done, "spawned task should complete and update status");
}

// ── Multiple spawns ─────────────────────────────────────────────

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

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

impl Component for MultiSpawn {
    fn render(&self, cx: &mut RenderCx) {
        cx.line(&format!("count: {}", self.get_count()));
    }
    fn mount(&mut self, cx: &mut EventCx) {
        for _ in 0..3 {
            cx.spawn(move || {
                std::thread::sleep(std::time::Duration::from_millis(5));
                "ok".to_string()
            });
        }
    }
    fn event(&mut self, event: &Event, cx: &mut EventCx) {
        if let Event::TaskComplete(ref result) = event {
            if result == "ok" {
                self.set_count(self.get_count() + 1, cx);
            }
        }
    }
}

#[test]
fn multiple_spawns_all_complete() {
    let mut pilot = Pilot::new(MultiSpawn::new(), 20, 3);
    let done = pilot.run_until(100, |buf| {
        buf.cells.iter().any(|c| c.symbol == "3")
    }).unwrap();
    assert!(done, "all 3 spawned tasks should complete");
}

// ── Worker error resilience ─────────────────────────────────────

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

impl PanicWorker {
    fn new() -> Self { Self { status: 0 } }
}

impl Component for PanicWorker {
    fn render(&self, cx: &mut RenderCx) {
        cx.line(&format!("status: {}", self.get_status()));
    }
    fn mount(&mut self, cx: &mut EventCx) {
        cx.spawn(move || "completed".to_string());
    }
    fn event(&mut self, event: &Event, cx: &mut EventCx) {
        if let Event::TaskComplete(ref result) = event {
            if !result.is_empty() {
                self.set_status(1, cx);
            }
        }
    }
}

#[test]
fn worker_completes_gracefully() {
    let mut pilot = Pilot::new(PanicWorker::new(), 20, 3);
    let done = pilot.run_until(100, |buf| {
        buf.cells.iter().any(|c| c.symbol == "1")
    }).unwrap();
    assert!(done, "worker should complete and deliver result");
}

// ── Key-triggered spawn ─────────────────────────────────────────

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

impl KeyTriggered {
    fn new() -> Self { Self { result: 0 } }
}

impl Component for KeyTriggered {
    fn render(&self, cx: &mut RenderCx) {
        cx.line(&format!("result: {}", self.get_result()));
    }
    fn event(&mut self, event: &Event, cx: &mut EventCx) {
        match event {
            Event::Key(key_event) if key_event.key == Key::Char('r') => {
                cx.spawn(move || "triggered".to_string());
            }
            Event::TaskComplete(ref result) => {
                if result == "triggered" {
                    self.set_result(1, cx);
                }
            }
            _ => {}
        }
    }
}

#[test]
fn spawn_triggered_by_key() {
    let mut pilot = Pilot::new(KeyTriggered::new(), 20, 3);
    pilot.press(Key::Char('r')).unwrap();
    let done = pilot.run_until(100, |buf| {
        buf.cells.iter().any(|c| c.symbol == "1")
    }).unwrap();
    assert!(done, "spawn triggered by key should complete");
}

// ── Data passing ────────────────────────────────────────────────

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

impl DataWorker {
    fn new() -> Self { Self { data: 0 } }
}

impl Component for DataWorker {
    fn render(&self, cx: &mut RenderCx) {
        cx.line(&format!("data: {}", self.get_data()));
    }
    fn mount(&mut self, cx: &mut EventCx) {
        cx.spawn(move || "42".to_string());
    }
    fn event(&mut self, event: &Event, cx: &mut EventCx) {
        if let Event::TaskComplete(ref result) = event {
            if let Ok(val) = result.parse::<i32>() {
                self.set_data(val, cx);
            }
        }
    }
}

#[test]
fn spawn_passes_data_back() {
    let mut pilot = Pilot::new(DataWorker::new(), 20, 3);
    let done = pilot.run_until(100, |buf| {
        buf.cells.iter().any(|c| c.symbol == "4")
    }).unwrap();
    assert!(done, "task should pass data back via TaskComplete");
}

// ── WorkerId type ───────────────────────────────────────────────

#[test]
fn worker_id_is_unique() {
    let id1 = WorkerId(1);
    let id2 = WorkerId(2);
    assert_ne!(id1, id2);
    assert_eq!(id1, WorkerId(1));
}

// ── spawn_worker with WorkerDone ───────────────────────────────

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

impl TrackedWorker {
    fn new() -> Self { Self { done: 0 } }
}

impl Component for TrackedWorker {
    fn render(&self, cx: &mut RenderCx) {
        cx.line(&format!("done: {}", self.get_done()));
    }
    fn mount(&mut self, cx: &mut EventCx) {
        cx.spawn_worker(move || {
            std::thread::sleep(std::time::Duration::from_millis(5));
            "tracked".to_string()
        });
    }
    fn event(&mut self, event: &Event, cx: &mut EventCx) {
        if let Event::WorkerDone(_, ref payload) = event {
            if payload == "tracked" {
                self.set_done(1, cx);
            }
        }
    }
}

#[test]
fn spawn_worker_delivers_workerdone() {
    let mut pilot = Pilot::new(TrackedWorker::new(), 20, 3);
    let done = pilot.run_until(100, |buf| {
        buf.cells.iter().any(|c| c.symbol == "1")
    }).unwrap();
    assert!(done, "spawn_worker should deliver WorkerDone event");
}

// ── cancel_worker ────────────────────────────────────────────────

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

impl CancellableWorker {
    fn new() -> Self { Self { result: 0 } }
}

impl Component for CancellableWorker {
    fn render(&self, cx: &mut RenderCx) {
        cx.line(&format!("result: {}", self.get_result()));
    }
    fn mount(&mut self, cx: &mut EventCx) {
        let id = cx.spawn_worker(move || {
            std::thread::sleep(std::time::Duration::from_millis(500));
            "not_coming".to_string()
        });
        cx.cancel_worker(id);
        cx.set_timer(20); // short timer to pump events
    }
    fn event(&mut self, event: &Event, cx: &mut EventCx) {
        match event {
            Event::WorkerDone(_, ref payload) => {
                if payload.is_empty() {
                    self.set_result(1, cx); // cancelled
                } else {
                    self.set_result(2, cx); // completed (should NOT happen)
                }
            }
            Event::Timer(_) => {
                if self.get_result() == 0 {
                    self.set_result(3, cx); // fallback timeout
                }
            }
            _ => {}
        }
    }
}

#[test]
fn cancel_worker_stops_execution() {
    let mut pilot = Pilot::new(CancellableWorker::new(), 20, 3);
    let updated = pilot.run_until(100, |buf| {
        buf.cells.iter().any(|c| c.symbol == "1" || c.symbol == "3")
    }).unwrap();
    assert!(updated, "cancel should produce cancelled WorkerDone or timeout");
    assert!(!pilot.frame().cells.iter().any(|c| c.symbol == "2"),
        "cancelled worker should not complete normally");
}