telar 0.1.5

A modular Rust UI framework with its own template language, reactive signals and a self-contained renderer.
Documentation
//! Integration test for `watch_path`: a real file change, a real platform watcher, and the callback landing
//! on the thread that asked for it.
//!
//! Nothing here can be unit-tested — the whole point of the helper is the hop from the watcher's thread to
//! the UI thread, and a test that stubbed the watcher would assert only that `spawn_stream` works.

use std::cell::Cell;
use std::rc::Rc;
use std::time::{Duration, Instant};

use telar::{drain_tasks, watch_path};

fn scratch(name: &str) -> std::path::PathBuf {
    let dir = std::env::temp_dir().join(format!("telar-watch-{name}-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).expect("create scratch dir");
    dir
}

/// Drains delivered task values until `hits` stops rising, or gives up. Stands in for the runner's per-frame
/// `drain_tasks`.
fn settle_until(hits: &Rc<Cell<usize>>, want: usize, timeout: Duration) -> usize {
    let deadline = Instant::now() + timeout;
    while hits.get() < want && Instant::now() < deadline {
        drain_tasks();
        std::thread::sleep(Duration::from_millis(10));
    }
    drain_tasks();
    hits.get()
}

#[test]
fn a_write_under_the_watched_directory_reaches_the_ui_thread() {
    let dir = scratch("write");
    let hits = Rc::new(Cell::new(0usize));

    let counted = Rc::clone(&hits);
    let watch = watch_path(&dir, move || counted.set(counted.get() + 1));

    // Give the platform watcher a moment to register before touching anything, or the change races the watch.
    std::thread::sleep(Duration::from_millis(200));
    std::fs::write(dir.join("config.toml"), "a = 1\n").unwrap();

    assert!(
        settle_until(&hits, 1, Duration::from_secs(10)) >= 1,
        "the write never reached the callback"
    );

    watch.cancel();
    let _ = std::fs::remove_dir_all(&dir);
}

/// A cancelled watch stops calling back. Without this the `Task` would be decoration: the callback usually
/// writes a signal belonging to state the caller is in the middle of tearing down.
#[test]
fn a_cancelled_watch_stops_reporting() {
    let dir = scratch("cancel");
    let hits = Rc::new(Cell::new(0usize));

    let counted = Rc::clone(&hits);
    let watch = watch_path(&dir, move || counted.set(counted.get() + 1));
    std::thread::sleep(Duration::from_millis(200));
    std::fs::write(dir.join("first.toml"), "a = 1\n").unwrap();
    let before = settle_until(&hits, 1, Duration::from_secs(10));
    assert!(before >= 1, "the watch never reported at all");

    watch.cancel();
    std::fs::write(dir.join("second.toml"), "b = 2\n").unwrap();
    std::thread::sleep(Duration::from_millis(300));
    drain_tasks();

    assert_eq!(hits.get(), before, "a cancelled watch kept reporting");
    let _ = std::fs::remove_dir_all(&dir);
}