yew/packages/yew/tests/suspense.rs
Kaede Hoshikawa 485a1b8c4a
Function Components & Hooks V2 (#2401)
* Make a use_hook hook with the new Hook trait.

* Implement Lifetime.

* Rewrites function signature.

* Only apply lifetime if there're other lifetimes.

* Cleanup signature rewrite logic.

* Rewrite hook body.

* Port some built-in hooks.

* Finish porting all built-in hooks.

* Port tests.

* Fix tests.

* Migrate to macro-based hooks.

* Fix HookContext, add tests on non-possible locations.

* Fix stderr for trybuild.

* Add 1 more test case.

* Adjust doc location.

* Pretty print hook signature.

* Fix Items & std::ops::Fn*.

* Add use_memo.

* Optimise Implementation of hooks.

* Use Box to capture function value only.

* Detect whether needs boxing.

* Add args if boxing not needed.

* Enforce hook number.

* Deduplicate use_effect.

* Optimise Implementation.

* Update documentation.

* Fix website test. Strip BoxedHook implementation from it.

* Allow doc string.

* Workaround doc tests.

* Optimise codebase & documentation.

* Fix website test.

* Reduce implementation complexity.

* Destructor is no more.

* Documentation and macros.

* Reduce heap allocation and hook complexity.

* Remove Queue as well.

* Prefer Generics.

* Fix typo.

* Remove more allocations.

* Add comments.

* Remove outdated comment.

* Bare Function Pointer for better code size.
2022-01-28 11:51:37 +02:00

582 lines
16 KiB
Rust

mod common;
use common::obtain_result;
use wasm_bindgen_test::*;
use yew::prelude::*;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
use std::cell::RefCell;
use std::rc::Rc;
use gloo::timers::future::TimeoutFuture;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::spawn_local;
use web_sys::{HtmlElement, HtmlTextAreaElement};
use yew::suspense::{Suspension, SuspensionResult};
#[wasm_bindgen_test]
async fn suspense_works() {
#[derive(PartialEq)]
pub struct SleepState {
s: Suspension,
}
impl SleepState {
fn new() -> Self {
let (s, handle) = Suspension::new();
spawn_local(async move {
TimeoutFuture::new(50).await;
handle.resume();
});
Self { s }
}
}
impl Reducible for SleepState {
type Action = ();
fn reduce(self: Rc<Self>, _action: Self::Action) -> Rc<Self> {
Self::new().into()
}
}
#[hook]
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
let sleep_state = use_reducer(SleepState::new);
if sleep_state.s.resumed() {
Ok(Rc::new(move || sleep_state.dispatch(())))
} else {
Err(sleep_state.s.clone())
}
}
#[function_component(Content)]
fn content() -> HtmlResult {
let resleep = use_sleep()?;
let value = use_state(|| 0);
let on_increment = {
let value = value.clone();
Callback::from(move |_: MouseEvent| {
value.set(*value + 1);
})
};
let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())());
Ok(html! {
<div class="content-area">
<div class="actual-result">{*value}</div>
<button class="increase" onclick={on_increment}>{"increase"}</button>
<div class="action-area">
<button class="take-a-break" onclick={on_take_a_break}>{"Take a break!"}</button>
</div>
</div>
})
}
#[function_component(App)]
fn app() -> Html {
let fallback = html! {<div>{"wait..."}</div>};
html! {
<div id="result">
<Suspense {fallback}>
<Content />
</Suspense>
</div>
}
}
yew::start_app_in_element::<App>(gloo_utils::document().get_element_by_id("output").unwrap());
TimeoutFuture::new(10).await;
let result = obtain_result();
assert_eq!(result.as_str(), "<div>wait...</div>");
TimeoutFuture::new(50).await;
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><div class="actual-result">0</div><button class="increase">increase</button><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
);
TimeoutFuture::new(10).await;
gloo_utils::document()
.query_selector(".increase")
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
.click();
gloo_utils::document()
.query_selector(".increase")
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
.click();
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><div class="actual-result">2</div><button class="increase">increase</button><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
);
gloo_utils::document()
.query_selector(".take-a-break")
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
.click();
TimeoutFuture::new(10).await;
let result = obtain_result();
assert_eq!(result.as_str(), "<div>wait...</div>");
TimeoutFuture::new(50).await;
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><div class="actual-result">2</div><button class="increase">increase</button><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
);
}
#[wasm_bindgen_test]
async fn suspense_not_suspended_at_start() {
#[derive(PartialEq)]
pub struct SleepState {
s: Option<Suspension>,
}
impl SleepState {
fn new() -> Self {
Self { s: None }
}
}
impl Reducible for SleepState {
type Action = ();
fn reduce(self: Rc<Self>, _action: Self::Action) -> Rc<Self> {
let (s, handle) = Suspension::new();
spawn_local(async move {
TimeoutFuture::new(50).await;
handle.resume();
});
Self { s: Some(s) }.into()
}
}
#[hook]
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
let sleep_state = use_reducer(SleepState::new);
let s = match sleep_state.s.clone() {
Some(m) => m,
None => return Ok(Rc::new(move || sleep_state.dispatch(()))),
};
if s.resumed() {
Ok(Rc::new(move || sleep_state.dispatch(())))
} else {
Err(s)
}
}
#[function_component(Content)]
fn content() -> HtmlResult {
let resleep = use_sleep()?;
let value = use_state(|| "I am writing a long story...".to_string());
let on_text_input = {
let value = value.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlTextAreaElement = e.target_unchecked_into();
value.set(input.value());
})
};
let on_take_a_break = Callback::from(move |_| (resleep.clone())());
Ok(html! {
<div class="content-area">
<textarea value={value.to_string()} oninput={on_text_input}></textarea>
<div class="action-area">
<button class="take-a-break" onclick={on_take_a_break}>{"Take a break!"}</button>
</div>
</div>
})
}
#[function_component(App)]
fn app() -> Html {
let fallback = html! {<div>{"wait..."}</div>};
html! {
<div id="result">
<Suspense {fallback}>
<Content />
</Suspense>
</div>
}
}
yew::start_app_in_element::<App>(gloo_utils::document().get_element_by_id("output").unwrap());
TimeoutFuture::new(10).await;
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><textarea></textarea><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
);
gloo_utils::document()
.query_selector(".take-a-break")
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
.click();
TimeoutFuture::new(10).await;
let result = obtain_result();
assert_eq!(result.as_str(), "<div>wait...</div>");
TimeoutFuture::new(50).await;
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><textarea></textarea><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
);
}
#[wasm_bindgen_test]
async fn suspense_nested_suspense_works() {
#[derive(PartialEq)]
pub struct SleepState {
s: Suspension,
}
impl SleepState {
fn new() -> Self {
let (s, handle) = Suspension::new();
spawn_local(async move {
TimeoutFuture::new(50).await;
handle.resume();
});
Self { s }
}
}
impl Reducible for SleepState {
type Action = ();
fn reduce(self: Rc<Self>, _action: Self::Action) -> Rc<Self> {
Self::new().into()
}
}
#[hook]
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
let sleep_state = use_reducer(SleepState::new);
if sleep_state.s.resumed() {
Ok(Rc::new(move || sleep_state.dispatch(())))
} else {
Err(sleep_state.s.clone())
}
}
#[function_component(InnerContent)]
fn inner_content() -> HtmlResult {
let resleep = use_sleep()?;
let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())());
Ok(html! {
<div class="content-area">
<div class="action-area">
<button class="take-a-break2" onclick={on_take_a_break}>{"Take a break!"}</button>
</div>
</div>
})
}
#[function_component(Content)]
fn content() -> HtmlResult {
let resleep = use_sleep()?;
let fallback = html! {<div>{"wait...(inner)"}</div>};
let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())());
Ok(html! {
<div class="content-area">
<div class="action-area">
<button class="take-a-break" onclick={on_take_a_break}>{"Take a break!"}</button>
</div>
<Suspense {fallback}>
<InnerContent />
</Suspense>
</div>
})
}
#[function_component(App)]
fn app() -> Html {
let fallback = html! {<div>{"wait...(outer)"}</div>};
html! {
<div id="result">
<Suspense {fallback}>
<Content />
</Suspense>
</div>
}
}
yew::start_app_in_element::<App>(gloo_utils::document().get_element_by_id("output").unwrap());
TimeoutFuture::new(10).await;
let result = obtain_result();
assert_eq!(result.as_str(), "<div>wait...(outer)</div>");
TimeoutFuture::new(50).await;
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><div>wait...(inner)</div></div>"#
);
TimeoutFuture::new(50).await;
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><div class="content-area"><div class="action-area"><button class="take-a-break2">Take a break!</button></div></div></div>"#
);
gloo_utils::document()
.query_selector(".take-a-break2")
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
.click();
TimeoutFuture::new(10).await;
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><div>wait...(inner)</div></div>"#
);
TimeoutFuture::new(50).await;
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><div class="content-area"><div class="action-area"><button class="take-a-break2">Take a break!</button></div></div></div>"#
);
}
#[wasm_bindgen_test]
async fn effects_not_run_when_suspended() {
#[derive(PartialEq)]
pub struct SleepState {
s: Suspension,
}
impl SleepState {
fn new() -> Self {
let (s, handle) = Suspension::new();
spawn_local(async move {
TimeoutFuture::new(50).await;
handle.resume();
});
Self { s }
}
}
impl Reducible for SleepState {
type Action = ();
fn reduce(self: Rc<Self>, _action: Self::Action) -> Rc<Self> {
Self::new().into()
}
}
#[hook]
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
let sleep_state = use_reducer(SleepState::new);
if sleep_state.s.resumed() {
Ok(Rc::new(move || sleep_state.dispatch(())))
} else {
Err(sleep_state.s.clone())
}
}
#[derive(Properties, Clone)]
struct Props {
counter: Rc<RefCell<u64>>,
}
impl PartialEq for Props {
fn eq(&self, _rhs: &Self) -> bool {
true
}
}
#[function_component(Content)]
fn content(props: &Props) -> HtmlResult {
{
let counter = props.counter.clone();
use_effect(move || {
let mut counter = counter.borrow_mut();
*counter += 1;
|| {}
});
}
let resleep = use_sleep()?;
let value = use_state(|| 0);
let on_increment = {
let value = value.clone();
Callback::from(move |_: MouseEvent| {
value.set(*value + 1);
})
};
let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())());
Ok(html! {
<div class="content-area">
<div class="actual-result">{*value}</div>
<button class="increase" onclick={on_increment}>{"increase"}</button>
<div class="action-area">
<button class="take-a-break" onclick={on_take_a_break}>{"Take a break!"}</button>
</div>
</div>
})
}
#[function_component(App)]
fn app(props: &Props) -> Html {
let fallback = html! {<div>{"wait..."}</div>};
html! {
<div id="result">
<Suspense {fallback}>
<Content counter={props.counter.clone()} />
</Suspense>
</div>
}
}
let counter = Rc::new(RefCell::new(0_u64));
let props = Props {
counter: counter.clone(),
};
yew::start_app_with_props_in_element::<App>(
gloo_utils::document().get_element_by_id("output").unwrap(),
props,
);
TimeoutFuture::new(10).await;
let result = obtain_result();
assert_eq!(result.as_str(), "<div>wait...</div>");
assert_eq!(*counter.borrow(), 0); // effects not called.
TimeoutFuture::new(50).await;
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><div class="actual-result">0</div><button class="increase">increase</button><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
);
assert_eq!(*counter.borrow(), 1); // effects ran 1 time.
TimeoutFuture::new(10).await;
gloo_utils::document()
.query_selector(".increase")
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
.click();
gloo_utils::document()
.query_selector(".increase")
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
.click();
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><div class="actual-result">2</div><button class="increase">increase</button><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
);
assert_eq!(*counter.borrow(), 3); // effects ran 3 times.
gloo_utils::document()
.query_selector(".take-a-break")
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
.click();
TimeoutFuture::new(10).await;
let result = obtain_result();
assert_eq!(result.as_str(), "<div>wait...</div>");
assert_eq!(*counter.borrow(), 3); // effects ran 3 times.
TimeoutFuture::new(50).await;
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><div class="actual-result">2</div><button class="increase">increase</button><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
);
assert_eq!(*counter.borrow(), 4); // effects ran 4 times.
}