Skip to content

Commit 8a222d7

Browse files
feat: add scheduler::flush() to replace sleep(Duration::ZERO) in tests (#4044)
1 parent 14a4c18 commit 8a222d7

15 files changed

Lines changed: 149 additions & 90 deletions

.github/workflows/main-checks.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ jobs:
105105
- name: Run tests - yew
106106
run: |
107107
cd packages/yew
108-
CHROMEDRIVER=$(which chromedriver) cargo test --features csr,hydration,ssr --target wasm32-unknown-unknown
109-
GECKODRIVER=$(which geckodriver) cargo test --features csr,hydration,ssr --target wasm32-unknown-unknown
108+
CHROMEDRIVER=$(which chromedriver) cargo test --features csr,hydration,ssr,test --target wasm32-unknown-unknown
109+
GECKODRIVER=$(which geckodriver) cargo test --features csr,hydration,ssr,test --target wasm32-unknown-unknown
110110
111111
- name: Run tests - yew-router
112112
run: |
@@ -254,7 +254,7 @@ jobs:
254254
- name: Run WASI tests for yew
255255
run: |
256256
RUST_LOG=info
257-
cargo test --features ssr,hydration --target wasm32-wasip1 -p yew
257+
cargo test --features ssr,hydration,test --target wasm32-wasip1 -p yew
258258
259259
example-runnable-tests-on-wasi:
260260
name: Example Runnable Tests on WASI

packages/yew/Makefile.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tasks.native-test]
22
command = "cargo"
3-
args = ["test", "--features", "csr,ssr,hydration"]
3+
args = ["test", "--features", "csr,ssr,hydration,test"]
44

55
[tasks.wasm-test]
66
command = "wasm-pack"
@@ -10,7 +10,7 @@ args = [
1010
"--headless",
1111
"--",
1212
"--features",
13-
"csr,hydration,ssr",
13+
"csr,hydration,ssr,test",
1414
]
1515

1616
[tasks.ssr-test]

packages/yew/src/scheduler.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,34 @@
33
use std::cell::RefCell;
44
use std::collections::BTreeMap;
55
use std::rc::Rc;
6+
#[cfg(any(test, feature = "test"))]
7+
mod flush_wakers {
8+
use std::cell::RefCell;
9+
use std::task::Waker;
10+
11+
thread_local! {
12+
static FLUSH_WAKERS: RefCell<Vec<Waker>> = Default::default();
13+
}
14+
15+
#[cfg(all(
16+
target_arch = "wasm32",
17+
not(target_os = "wasi"),
18+
not(feature = "not_browser_env")
19+
))]
20+
pub(super) fn register(waker: Waker) {
21+
FLUSH_WAKERS.with(|w| {
22+
w.borrow_mut().push(waker);
23+
});
24+
}
25+
26+
pub(super) fn wake_all() {
27+
FLUSH_WAKERS.with(|w| {
28+
for waker in w.borrow_mut().drain(..) {
29+
waker.wake();
30+
}
31+
});
32+
}
33+
}
634

735
/// Alias for `Rc<RefCell<T>>`
836
pub type Shared<T> = Rc<RefCell<T>>;
@@ -207,6 +235,8 @@ pub(crate) fn start_now() {
207235
LOCK.with(|l| {
208236
if let Ok(_lock) = l.try_borrow_mut() {
209237
scheduler_loop();
238+
#[cfg(any(test, feature = "test"))]
239+
flush_wakers::wake_all();
210240
}
211241
});
212242
}
@@ -232,6 +262,11 @@ mod arch {
232262
IS_SCHEDULED.store(is, Ordering::Relaxed)
233263
}
234264

265+
#[cfg(any(test, feature = "test"))]
266+
pub(super) fn is_scheduled() -> bool {
267+
check_scheduled()
268+
}
269+
235270
/// We delay the start of the scheduler to the end of the micro task queue.
236271
/// So any messages that needs to be queued can be queued.
237272
pub(crate) fn start() {
@@ -264,6 +299,51 @@ mod arch {
264299

265300
pub(crate) use arch::*;
266301

302+
/// Flush all pending scheduler work, ensuring all rendering and lifecycle callbacks complete.
303+
///
304+
/// On browser WebAssembly targets, the scheduler defers its work to the microtask queue.
305+
/// This function registers a waker that is notified when `start_now()` finishes draining all
306+
/// queues, providing proper event-driven render-complete notification without arbitrary sleeps.
307+
///
308+
/// On non-browser targets, the scheduler runs synchronously so this simply drains pending work.
309+
///
310+
/// Use this in tests after mounting or updating a component to ensure all rendering has
311+
/// completed before making assertions.
312+
#[cfg(all(
313+
any(test, feature = "test"),
314+
target_arch = "wasm32",
315+
not(target_os = "wasi"),
316+
not(feature = "not_browser_env")
317+
))]
318+
pub async fn flush() {
319+
std::future::poll_fn(|cx| {
320+
start_now();
321+
322+
if arch::is_scheduled() {
323+
flush_wakers::register(cx.waker().clone());
324+
std::task::Poll::Pending
325+
} else {
326+
std::task::Poll::Ready(())
327+
}
328+
})
329+
.await
330+
}
331+
332+
/// Flush all pending scheduler work, ensuring all rendering and lifecycle callbacks complete.
333+
///
334+
/// On non-browser targets, the scheduler runs synchronously so this simply drains pending work.
335+
#[cfg(all(
336+
any(test, feature = "test"),
337+
not(all(
338+
target_arch = "wasm32",
339+
not(target_os = "wasi"),
340+
not(feature = "not_browser_env")
341+
))
342+
))]
343+
pub async fn flush() {
344+
start_now();
345+
}
346+
267347
impl Scheduler {
268348
/// Fill vector with tasks to be executed according to Runnable type execution priority
269349
///

packages/yew/tests/hydration.rs

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use yew::platform::time::sleep;
1616
use yew::prelude::*;
1717
use yew::suspense::{use_future, Suspension, SuspensionResult};
1818
use yew::virtual_dom::VNode;
19-
use yew::{component, Renderer, ServerRenderer};
19+
use yew::{component, scheduler, Renderer, ServerRenderer};
2020

2121
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
2222

@@ -62,12 +62,12 @@ async fn hydration_works() {
6262
.unwrap()
6363
.set_inner_html(&s);
6464

65-
sleep(Duration::ZERO).await;
65+
scheduler::flush().await;
6666

6767
Renderer::<App>::with_root(gloo::utils::document().get_element_by_id("output").unwrap())
6868
.hydrate();
6969

70-
sleep(Duration::ZERO).await;
70+
scheduler::flush().await;
7171

7272
let result = obtain_result_by_id("output");
7373

@@ -85,7 +85,7 @@ async fn hydration_works() {
8585
.unwrap()
8686
.click();
8787

88-
sleep(Duration::ZERO).await;
88+
scheduler::flush().await;
8989

9090
let result = obtain_result_by_id("output");
9191

@@ -237,7 +237,7 @@ async fn hydration_with_suspense() {
237237
.unwrap()
238238
.set_inner_html(&s);
239239

240-
sleep(Duration::ZERO).await;
240+
scheduler::flush().await;
241241

242242
Renderer::<App>::with_root(gloo::utils::document().get_element_by_id("output").unwrap())
243243
.hydrate();
@@ -393,7 +393,7 @@ async fn hydration_with_suspense_not_suspended_at_start() {
393393
.unwrap()
394394
.set_inner_html(&s);
395395

396-
sleep(Duration::ZERO).await;
396+
scheduler::flush().await;
397397

398398
Renderer::<App>::with_root(gloo::utils::document().get_element_by_id("output").unwrap())
399399
.hydrate();
@@ -524,7 +524,7 @@ async fn hydration_nested_suspense_works() {
524524
.unwrap()
525525
.set_inner_html(&s);
526526

527-
sleep(Duration::ZERO).await;
527+
scheduler::flush().await;
528528

529529
Renderer::<App>::with_root(gloo::utils::document().get_element_by_id("output").unwrap())
530530
.hydrate();
@@ -661,12 +661,12 @@ async fn hydration_node_ref_works() {
661661
.unwrap()
662662
.set_inner_html(&s);
663663

664-
sleep(Duration::ZERO).await;
664+
scheduler::flush().await;
665665

666666
Renderer::<App>::with_root(gloo::utils::document().get_element_by_id("output").unwrap())
667667
.hydrate();
668668

669-
sleep(Duration::ZERO).await;
669+
scheduler::flush().await;
670670

671671
let result = obtain_result_by_id("output");
672672
assert_eq!(
@@ -682,7 +682,7 @@ async fn hydration_node_ref_works() {
682682
.unwrap()
683683
.click();
684684

685-
sleep(Duration::ZERO).await;
685+
scheduler::flush().await;
686686

687687
let result = obtain_result_by_id("output");
688688
assert_eq!(
@@ -754,16 +754,13 @@ async fn hydration_list_order_works() {
754754
.unwrap()
755755
.set_inner_html(&s);
756756

757-
sleep(Duration::ZERO).await;
757+
scheduler::flush().await;
758758

759759
Renderer::<App>::with_root(gloo::utils::document().get_element_by_id("output").unwrap())
760760
.hydrate();
761761

762762
// Wait until all suspended components becomes revealed.
763-
sleep(Duration::ZERO).await;
764-
sleep(Duration::ZERO).await;
765-
sleep(Duration::ZERO).await;
766-
sleep(Duration::ZERO).await;
763+
scheduler::flush().await;
767764

768765
let result = obtain_result_by_id("output");
769766
assert_eq!(
@@ -837,13 +834,13 @@ async fn hydration_suspense_no_flickering() {
837834
.unwrap()
838835
.set_inner_html(&s);
839836

840-
sleep(Duration::ZERO).await;
837+
scheduler::flush().await;
841838

842839
Renderer::<App>::with_root(gloo::utils::document().get_element_by_id("output").unwrap())
843840
.hydrate();
844841

845842
// Wait until all suspended components becomes revealed.
846-
sleep(Duration::ZERO).await;
843+
scheduler::flush().await;
847844

848845
let result = obtain_result_by_id("output");
849846
assert_eq!(
@@ -950,16 +947,13 @@ async fn hydration_order_issue_nested_suspense() {
950947
.unwrap()
951948
.set_inner_html(&s);
952949

953-
sleep(Duration::ZERO).await;
950+
scheduler::flush().await;
954951

955952
Renderer::<App>::with_root(gloo::utils::document().get_element_by_id("output").unwrap())
956953
.hydrate();
957954

958955
// Wait until all suspended components becomes revealed.
959-
sleep(Duration::ZERO).await;
960-
sleep(Duration::ZERO).await;
961-
sleep(Duration::ZERO).await;
962-
sleep(Duration::ZERO).await;
956+
scheduler::flush().await;
963957

964958
let result = obtain_result_by_id("output");
965959
assert_eq!(
@@ -1180,7 +1174,7 @@ async fn hydration_with_camelcase_svg_elements() {
11801174
.unwrap()
11811175
.set_inner_html(&s);
11821176

1183-
sleep(Duration::ZERO).await;
1177+
scheduler::flush().await;
11841178

11851179
// Hydrate - this should not panic
11861180
Renderer::<App>::with_root(gloo::utils::document().get_element_by_id("output").unwrap())
@@ -1268,15 +1262,12 @@ async fn hydration_suspended_child_does_not_trap_sibling_slot() {
12681262
.unwrap()
12691263
.set_inner_html(&s);
12701264

1271-
sleep(Duration::ZERO).await;
1265+
scheduler::flush().await;
12721266

12731267
Renderer::<App>::with_root(gloo::utils::document().get_element_by_id("output").unwrap())
12741268
.hydrate();
12751269

1276-
sleep(Duration::ZERO).await;
1277-
sleep(Duration::ZERO).await;
1278-
sleep(Duration::ZERO).await;
1279-
sleep(Duration::ZERO).await;
1270+
scheduler::flush().await;
12801271

12811272
let result = obtain_result();
12821273

packages/yew/tests/mod.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22

33
mod common;
44

5-
use std::time::Duration;
6-
75
use common::obtain_result;
86
use wasm_bindgen_test::*;
9-
use yew::platform::time::sleep;
107
use yew::prelude::*;
8+
use yew::scheduler;
119

1210
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
1311

@@ -36,7 +34,7 @@ async fn props_are_passed() {
3634
)
3735
.render();
3836

39-
sleep(Duration::ZERO).await;
37+
scheduler::flush().await;
4038
let result = obtain_result();
4139
assert_eq!(result.as_str(), "done");
4240
}

packages/yew/tests/suspense.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use yew::platform::spawn_local;
1414
use yew::platform::time::sleep;
1515
use yew::prelude::*;
1616
use yew::suspense::{use_future, use_future_with, Suspension, SuspensionResult};
17-
use yew::UseStateHandle;
17+
use yew::{scheduler, UseStateHandle};
1818

1919
wasm_bindgen_test_configure!(run_in_browser);
2020

@@ -123,7 +123,7 @@ async fn suspense_works() {
123123
.unwrap()
124124
.click();
125125

126-
sleep(Duration::ZERO).await;
126+
scheduler::flush().await;
127127

128128
gloo::utils::document()
129129
.query_selector(".increase")
@@ -553,7 +553,7 @@ async fn effects_not_run_when_suspended() {
553553
.unwrap()
554554
.click();
555555

556-
sleep(Duration::ZERO).await;
556+
scheduler::flush().await;
557557

558558
gloo::utils::document()
559559
.query_selector(".increase")
@@ -563,7 +563,7 @@ async fn effects_not_run_when_suspended() {
563563
.unwrap()
564564
.click();
565565

566-
sleep(Duration::from_millis(0)).await;
566+
scheduler::flush().await;
567567

568568
let result = obtain_result();
569569
assert_eq!(
@@ -788,7 +788,7 @@ async fn test_duplicate_suspension() {
788788
#[component]
789789
fn FetchingProvider(props: &ChildrenProps) -> HtmlResult {
790790
use_future(|| async {
791-
sleep(Duration::ZERO).await;
791+
scheduler::flush().await;
792792
})?;
793793
Ok(html! { <>{props.children.clone()}</> })
794794
}

0 commit comments

Comments
 (0)