use crate::reactive::RwSignal;
use crate::reactive::{__reset_for_tests, flush, resource, resource_sync, Owner, ResourceState};
use crate::tasks;
fn with_test_owner<R>(f: impl FnOnce() -> R) -> R {
__reset_for_tests();
tasks::__reset_for_tests();
let owner = Owner::new(None);
owner.with(f)
}
#[test]
fn resource_sync_ready_state_for_ok_fetch() {
with_test_owner(|| {
let r = resource_sync(|| Ok::<_, String>(42_i32));
assert!(matches!(r.state(), ResourceState::Ready(42)));
assert_eq!(r.get(), Some(42));
assert!(!r.loading());
assert!(r.error().is_none());
});
}
#[test]
fn resource_sync_error_state_for_err_fetch() {
with_test_owner(|| {
let r = resource_sync(|| Err::<i32, _>("oops".to_string()));
assert!(matches!(r.state(), ResourceState::Error(_)));
assert_eq!(r.get(), None);
assert!(!r.loading());
assert_eq!(r.error().as_deref(), Some("oops"));
});
}
#[test]
fn async_resource_starts_in_loading_state() {
with_test_owner(|| {
let r = resource::<i32, _, _>(|| async { Ok(7) });
assert!(r.loading());
assert!(matches!(r.state(), ResourceState::Loading));
assert_eq!(r.get(), None);
assert!(r.error().is_none());
});
}
#[test]
fn async_resource_transitions_to_ready_after_tick() {
with_test_owner(|| {
let r = resource::<i32, _, _>(|| async { Ok(99) });
tasks::run_until_stalled();
assert!(matches!(r.state(), ResourceState::Ready(99)));
assert_eq!(r.get(), Some(99));
assert!(!r.loading());
});
}
#[test]
fn async_resource_transitions_to_error_on_err_result() {
with_test_owner(|| {
let r = resource::<i32, _, _>(|| async { Err("boom".to_string()) });
tasks::run_until_stalled();
assert!(matches!(r.state(), ResourceState::Error(_)));
assert_eq!(r.error().as_deref(), Some("boom"));
assert!(!r.loading());
});
}
#[test]
fn async_resource_with_pending_future_stays_loading() {
use std::pin::Pin;
use std::task::{Context, Poll};
struct NeverReady;
impl std::future::Future for NeverReady {
type Output = Result<i32, String>;
fn poll(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Pending
}
}
with_test_owner(|| {
let r = resource::<i32, _, _>(|| NeverReady);
tasks::run_until_stalled();
assert!(r.loading(), "never-ready future must keep resource Loading");
});
}
#[test]
fn async_resource_multi_step_future_completes_within_one_tick() {
use std::pin::Pin;
use std::task::{Context, Poll};
struct OneYield(bool);
impl std::future::Future for OneYield {
type Output = Result<i32, String>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if !self.0 {
self.0 = true;
cx.waker().wake_by_ref();
Poll::Pending
} else {
Poll::Ready(Ok(123))
}
}
}
with_test_owner(|| {
let r = resource::<i32, _, _>(|| OneYield(false));
tasks::run_until_stalled();
assert_eq!(r.get(), Some(123));
});
}
#[test]
fn async_resource_tracks_signal_read_in_sync_prefix_and_refetches() {
with_test_owner(|| {
let query = RwSignal::new(String::new());
let r = resource::<String, _, _>(move || {
let q = query.get();
async move {
if q.trim().is_empty() {
Ok(String::new())
} else {
Ok(q.to_uppercase())
}
}
});
tasks::run_until_stalled();
assert_eq!(r.get().as_deref(), Some(""));
query.set("Slime".to_string());
flush(); tasks::run_until_stalled(); assert_eq!(
r.get().as_deref(),
Some("SLIME"),
"resource must re-fetch with the new query after sync-prefix read changes"
);
query.set("Goo".to_string());
flush();
tasks::run_until_stalled();
assert_eq!(r.get().as_deref(), Some("GOO"));
});
}
#[test]
fn async_resource_tracks_signal_read_after_await_and_refetches() {
use std::pin::Pin;
use std::task::{Context, Poll};
struct OneYield(bool);
impl std::future::Future for OneYield {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
if !self.0 {
self.0 = true;
cx.waker().wake_by_ref();
Poll::Pending
} else {
Poll::Ready(())
}
}
}
with_test_owner(|| {
let multiplier = RwSignal::new(2_i32);
let r = resource::<i32, _, _>(move || async move {
OneYield(false).await;
let m = multiplier.get();
Ok(m * 10)
});
tasks::run_until_stalled();
assert_eq!(
r.get(),
Some(20),
"first fetch should suspend, resume, read multiplier=2 → 20"
);
multiplier.set(5);
flush();
tasks::run_until_stalled();
assert_eq!(
r.get(),
Some(50),
"resource must track a signal read AFTER an .await and re-fetch on change"
);
});
}
#[test]
fn async_resource_returns_to_loading_during_refetch() {
use std::pin::Pin;
use std::task::{Context, Poll};
struct OneYield(bool);
impl std::future::Future for OneYield {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
if !self.0 {
self.0 = true;
cx.waker().wake_by_ref();
Poll::Pending
} else {
Poll::Ready(())
}
}
}
with_test_owner(|| {
let n = RwSignal::new(1_i32);
let r = resource::<i32, _, _>(move || {
let v = n.get();
async move {
OneYield(false).await;
Ok(v)
}
});
tasks::run_until_stalled();
assert_eq!(r.get(), Some(1));
n.set(2);
flush();
assert!(
r.loading(),
"resource should be Loading during an in-flight re-fetch"
);
tasks::run_until_stalled();
assert_eq!(r.get(), Some(2));
});
}
#[test]
fn resource_state_helpers_match_active_branch() {
let loading: ResourceState<i32> = ResourceState::Loading;
assert!(loading.is_loading());
assert!(!loading.is_ready());
assert!(!loading.is_error());
let ready: ResourceState<i32> = ResourceState::Ready(1);
assert!(!ready.is_loading());
assert!(ready.is_ready());
assert!(!ready.is_error());
let err: ResourceState<i32> = ResourceState::Error("x".into());
assert!(!err.is_loading());
assert!(!err.is_ready());
assert!(err.is_error());
}