use egui_async::{
Bind, State, StateWithData,
bind::{CURR_FRAME, LAST_FRAME},
};
use std::sync::{Mutex, OnceLock};
fn test_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
fn with_lock<T>(f: impl FnOnce() -> T) -> T {
let _keep = match test_lock().lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
};
f()
}
fn set_frame_times(curr: f64, last: f64) {
CURR_FRAME.store(curr, std::sync::atomic::Ordering::Relaxed);
LAST_FRAME.store(last, std::sync::atomic::Ordering::Relaxed);
}
fn bump_frame<T: 'static, E: 'static>(b: &mut Bind<T, E>) {
let curr = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
LAST_FRAME.store(curr, std::sync::atomic::Ordering::Relaxed);
CURR_FRAME.store(curr + 1.0, std::sync::atomic::Ordering::Relaxed);
b.poll();
}
fn drive_until_finished<T: 'static, E: 'static>(b: &mut Bind<T, E>, max_frames: usize) -> bool {
use std::{thread, time::Duration};
for _ in 0..max_frames {
bump_frame(b);
if b.is_finished() {
return true;
}
thread::sleep(Duration::from_millis(2));
}
false
}
#[test]
fn request_ok_flow() {
with_lock(|| {
set_frame_times(0.0, -1.0);
let mut b: Bind<u32, String> = Bind::new(false);
assert_eq!(b.get_state(), State::Idle);
assert!(b.read().is_none());
b.request(async { Ok::<_, String>(42) });
assert_eq!(b.get_state(), State::Pending);
assert!(!b.is_finished());
assert!(
drive_until_finished(&mut b, 200),
"future did not finish in time"
);
assert!(b.is_finished());
assert!(b.just_completed());
let r = b.read().as_ref().expect("expected Some(..)");
assert_eq!(r, &Ok(42));
let taken = b.take();
assert!(matches!(taken, Some(Ok(42))));
assert!(b.is_idle());
assert!(b.read().is_none());
assert_eq!(b.count_executed(), 1);
});
}
#[test]
fn request_err_flow() {
with_lock(|| {
set_frame_times(0.0, -1.0);
let mut b: Bind<u32, String> = Bind::default();
b.request(async { Err::<u32, _>("nope".to_string()) });
assert_eq!(b.get_state(), State::Pending);
assert!(drive_until_finished(&mut b, 200));
match b.state() {
StateWithData::Failed(e) => assert_eq!(e, "nope"),
_ => panic!("expected Failed(..) state"),
}
assert!(b.is_err());
assert!(!b.is_ok());
});
}
#[test]
fn just_started_and_just_completed_flags() {
with_lock(|| {
set_frame_times(5.0, 4.0);
let mut b: Bind<&'static str, &'static str> = Bind::new(true);
b.request(async { Ok::<_, _>("done") });
assert!(b.just_started());
bump_frame(&mut b);
assert!(!b.just_started());
assert!(drive_until_finished(&mut b, 200));
assert!(b.just_completed());
bump_frame(&mut b);
assert!(!b.just_completed());
});
}
#[test]
fn refresh_restarts_and_replaces_data() {
with_lock(|| {
set_frame_times(0.0, -1.0);
let mut b: Bind<i32, String> = Bind::new(false);
b.fill(Ok(7));
assert!(b.is_finished());
assert!(matches!(b.read(), Some(Ok(7))));
b.refresh(async { Ok::<_, String>(99) });
assert_eq!(b.get_state(), State::Pending);
assert!(b.read().is_none());
assert!(drive_until_finished(&mut b, 200));
assert!(matches!(b.read(), Some(Ok(99))));
});
}
#[test]
fn take_consumes_and_resets() {
with_lock(|| {
set_frame_times(0.0, -1.0);
let mut b: Bind<String, String> = Bind::default();
b.fill(Ok("hello".to_string()));
let got = b.take();
assert!(matches!(got, Some(Ok(s)) if s == "hello"));
assert!(b.is_idle());
assert!(b.read().is_none());
});
}
#[test]
fn read_or_request_semantics() {
with_lock(|| {
set_frame_times(0.0, -1.0);
let mut b: Bind<&'static str, &'static str> = Bind::new(false);
let first = b.read_or_request(|| async { Ok::<_, _>("val") });
assert!(first.is_none());
assert_eq!(b.get_state(), State::Pending);
assert!(drive_until_finished(&mut b, 200));
let read = b
.read_or_request(|| async { unreachable!() })
.expect("expected Some after completion");
assert_eq!(read.as_deref(), Ok("val"));
});
}
#[test]
fn read_as_mut_allows_mutation() {
with_lock(|| {
set_frame_times(0.0, -1.0);
let mut b: Bind<String, String> = Bind::default();
b.fill(Ok("abc".into()));
if let Some(Ok(s)) = b.read_as_mut() {
s.push_str("123");
} else {
panic!("expected Some(Ok(_))");
}
assert!(matches!(b.read(), Some(Ok(s)) if s == "abc123"));
});
}
#[test]
fn retain_false_clears_when_not_drawn_last_frame() {
with_lock(|| {
set_frame_times(0.0, -1.0);
let mut b: Bind<i32, String> = Bind::new(false);
b.fill(Ok(123));
assert!(b.is_finished());
assert!(matches!(b.read(), Some(Ok(123))));
LAST_FRAME.store(1.0, std::sync::atomic::Ordering::Relaxed);
CURR_FRAME.store(2.0, std::sync::atomic::Ordering::Relaxed);
b.poll();
assert!(b.is_idle());
assert!(b.read().is_none());
});
}
#[test]
fn request_every_sec_behaves_as_timer() {
with_lock(|| {
set_frame_times(0.0, -1.0);
let mut b: Bind<&'static str, String> = Bind::new(false);
let dt = b.request_every_sec(|| async { Ok::<_, String>("tick") }, 10.0);
assert!(
dt.is_sign_negative(),
"expected negative/overdue first interval"
);
assert_eq!(b.count_executed(), 1);
assert_eq!(b.get_state(), State::Pending);
assert!(drive_until_finished(&mut b, 200));
let t_until = b.request_every_sec(|| async { Ok::<_, String>("tick2") }, 10.0);
assert!(
(9.0..=10.0).contains(&t_until),
"expected ~10s remaining, got {t_until}"
);
let now = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
LAST_FRAME.store(now, std::sync::atomic::Ordering::Relaxed);
CURR_FRAME.store(now + 11.0, std::sync::atomic::Ordering::Relaxed);
let overdue = b.request_every_sec(|| async { Ok::<_, String>("tick2") }, 10.0);
assert!(
overdue.is_sign_negative(),
"should be overdue and re-trigger"
);
assert_eq!(b.count_executed(), 2);
assert_eq!(b.get_state(), State::Pending);
});
}
#[cfg(not(target_family = "wasm"))]
#[test]
fn clear_while_pending_discards_result() {
use tokio::sync::oneshot;
with_lock(|| {
use std::{thread, time::Duration};
set_frame_times(0.0, -1.0);
let mut b: Bind<&'static str, String> = Bind::new(false);
let (gate_tx, gate_rx) = oneshot::channel::<()>();
b.request(async move {
let _ = gate_rx.await;
Ok::<_, String>("late")
});
assert_eq!(b.get_state(), State::Pending);
b.clear();
assert!(b.is_idle());
assert!(b.read().is_none());
let _ = gate_tx.send(());
thread::sleep(Duration::from_millis(5));
for _ in 0..5 {
bump_frame(&mut b);
assert!(b.is_idle());
assert!(b.read().is_none());
}
b.request(async { Ok::<_, String>("fresh") });
assert!(drive_until_finished(&mut b, 200));
assert!(matches!(b.read(), Some(Ok("fresh"))));
});
}