diff --git a/.github/workflows/main-checks.yml b/.github/workflows/main-checks.yml index 818d33e47da..e45d922f6c6 100644 --- a/.github/workflows/main-checks.yml +++ b/.github/workflows/main-checks.yml @@ -33,6 +33,7 @@ jobs: cargo clippy --features=ssr -- --deny=warnings cargo clippy --features=csr -- --deny=warnings cargo clippy --features=hydration -- --deny=warnings + cargo clippy --features=test_runner -- --deny=warnings cargo clippy --all-features --all-targets -- --deny=warnings working-directory: packages/yew @@ -64,6 +65,7 @@ jobs: cargo clippy --release --features=ssr -- --deny=warnings cargo clippy --release --features=csr -- --deny=warnings cargo clippy --release --features=hydration -- --deny=warnings + cargo clippy --release --features=test_runner -- --deny=warnings cargo clippy --release --all-features --all-targets -- --deny=warnings working-directory: packages/yew @@ -159,8 +161,8 @@ jobs: - name: Run tests - yew run: | cd packages/yew - CHROMEDRIVER=$(which chromedriver) cargo test --features csr,hydration,ssr --target wasm32-unknown-unknown - GECKODRIVER=$(which geckodriver) cargo test --features csr,hydration,ssr --target wasm32-unknown-unknown + CHROMEDRIVER=$(which chromedriver) cargo test --features test_runner,hydration,ssr --target wasm32-unknown-unknown + GECKODRIVER=$(which geckodriver) cargo test --features test_runner,hydration,ssr --target wasm32-unknown-unknown - name: Run tests - yew-router run: | @@ -202,7 +204,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: -p yew --features "csr,ssr,hydration,tokio" + args: -p yew --all-features test-lints: name: Test lints on nightly diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 491a44695dd..dc5eabae37c 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -72,9 +72,10 @@ wasm-bindgen-futures = "0.4" tokio = { version = "1.15.0", features = ["rt"], optional = true } [dev-dependencies] -wasm-bindgen-test = "0.3" +futures = { version = "0.3" } gloo = { version = "0.7", features = ["futures"] } gloo-net = { version = "0.2", features = ["http"] } +wasm-bindgen-test = "0.3" wasm-bindgen-futures = "0.4" rustversion = "1" trybuild = "1" @@ -92,6 +93,7 @@ features = [ ssr = ["futures", "html-escape"] # dep:html-escape csr = [] hydration = ["csr"] +test_runner = ["csr"] default = [] [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] diff --git a/packages/yew/src/app_handle.rs b/packages/yew/src/app_handle.rs index 1ed40396c0b..de248bc9529 100644 --- a/packages/yew/src/app_handle.rs +++ b/packages/yew/src/app_handle.rs @@ -5,8 +5,9 @@ use std::rc::Rc; use web_sys::Element; -use crate::dom_bundle::BSubtree; +use crate::dom_bundle::{BSubtree, Bundle}; use crate::html::{BaseComponent, NodeRef, Scope, Scoped}; +use crate::virtual_dom::VChild; /// An instance of an application. #[cfg_attr(documenting, doc(cfg(feature = "csr")))] @@ -26,19 +27,21 @@ where /// will render the model to a virtual DOM tree. pub(crate) fn mount_with_props(host: Element, props: Rc) -> Self { clear_element(&host); - let app = Self { - scope: Scope::new(None), - }; + let mut bundle = Bundle::new(); + let parent_scope = Scope::::new(None).to_any(); let hosting_root = BSubtree::create_root(&host); - app.scope.mount_in_place( - hosting_root, - host, + let scope = bundle.reconcile_vchild( + &hosting_root, + &parent_scope, + &host, NodeRef::default(), - NodeRef::default(), - props, + VChild { + props, + node_ref: NodeRef::default(), + key: None, + }, ); - - app + Self { scope } } /// Schedule the app for destruction diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs index fb7dec3ba97..96617d5ace5 100644 --- a/packages/yew/src/dom_bundle/bcomp.rs +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -29,6 +29,11 @@ impl BComp { pub fn key(&self) -> Option<&Key> { self.key.as_ref() } + + /// Get the scope of the underlying component + pub fn scope(&self) -> AnyScope { + self.scope.to_any() + } } impl fmt::Debug for BComp { @@ -490,7 +495,7 @@ mod layout_tests { use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - use crate::tests::layout_tests::{diff_layouts, TestLayout}; + use crate::tests::{TestCase, TestRunner}; use crate::{html, Children, Component, Context, Html, Properties}; wasm_bindgen_test_configure!(run_in_browser); @@ -530,221 +535,196 @@ mod layout_tests { struct B; #[test] - fn diff() { - let layout1 = TestLayout { - name: "1", - node: html! { - > - >> - {"C"} - > - }, - expected: "C", - }; - - let layout2 = TestLayout { - name: "2", - node: html! { - > - {"A"} - > - }, - expected: "A", - }; - - let layout3 = TestLayout { - name: "3", - node: html! { - > - >> + async fn diff() { + let mut trun = TestRunner::new(); + + trun.render(html! { + > + >> + {"C"} + > + }) + .await + .assert_inner_html("C"); + + trun.render(html! { + > + {"A"} + > + }) + .await + .assert_inner_html("A"); + + trun.render(html! { + > + >> + {"B"} + > + }) + .await + .assert_inner_html("B"); + + trun.render(html! { + > + >{"A"}> + {"B"} + > + }) + .await + .assert_inner_html("AB"); + + trun.render(html! { + > + <> + > + {"A"} + > + + {"B"} + > + }) + .await + .assert_inner_html("AB"); + + trun.render(html! { + > + <> + > + {"A"} + > {"B"} - > - }, - expected: "B", - }; - - let layout4 = TestLayout { - name: "4", - node: html! { - > - >{"A"}> - {"B"} - > - }, - expected: "AB", - }; - - let layout5 = TestLayout { - name: "5", - node: html! { - > - <> + + {"C"} + > + }) + .await + .assert_inner_html("ABC"); + + trun.render(html! { + > + <> + > + {"A"} + > + > + {"B"} + > + + {"C"} + > + }) + .await + .assert_inner_html("ABC"); + + trun.render(html! { + > + <> + > + {"A"} + > + > > - {"A"} + {"B"} > - - {"B"} - > - }, - expected: "AB", - }; - - let layout6 = TestLayout { - name: "6", - node: html! { - > + > + + {"C"} + > + }) + .await + .assert_inner_html("ABC"); + + trun.render(html! { + > + <> <> - > - {"A"} - > - {"B"} + {"A"} - {"C"} - > - }, - expected: "ABC", - }; - - let layout7 = TestLayout { - name: "7", - node: html! { - > - <> - > - {"A"} - > + > > {"B"} > - - {"C"} - > - }, - expected: "ABC", - }; - - let layout8 = TestLayout { - name: "8", - node: html! { - > - <> + > + + {"C"} + > + }) + .await + .assert_inner_html("ABC"); + + trun.render(html! { + > + <> + > > {"A"} > - > - > - {"B"} - > - > - - {"C"} - > - }, - expected: "ABC", - }; - - let layout9 = TestLayout { - name: "9", - node: html! { - > + > <> - <> - {"A"} - - > - > - {"B"} - > - > + {"B"} - {"C"} - > - }, - expected: "ABC", - }; - - let layout10 = TestLayout { - name: "10", - node: html! { - > + + {"C"} + > + }) + .await + .assert_inner_html("ABC"); + + trun.render(html! { + > + <> <> > > {"A"} > - > - <> {"B"} - + > - {"C"} - > - }, - expected: "ABC", - }; - - let layout11 = TestLayout { - name: "11", - node: html! { - > + + {"C"} + > + }) + .await + .assert_inner_html("ABC"); + + trun.render(html! { + > + <> + >> <> - <> - > + > + <> > {"A"} > - {"B"} - > - - - {"C"} - > - }, - expected: "ABC", - }; - - let layout12 = TestLayout { - name: "12", - node: html! { - > - <> - >> - <> - > - <> - > - {"A"} - > + <> + > + >> <> - > - >> - <> - {"B"} - <> - >> - > - - > - <> - - >> + {"B"} + <> + >> + > + + > + <> - {"C"} >> - <> - > - }, - expected: "ABC", - }; - - diff_layouts(vec![ - layout1, layout2, layout3, layout4, layout5, layout6, layout7, layout8, layout9, - layout10, layout11, layout12, - ]); + + {"C"} + >> + <> + > + }) + .await + .assert_inner_html("ABC"); + + trun.run_replayable_tests().await; } #[test] - fn component_with_children() { + async fn component_with_children() { #[derive(Properties, PartialEq)] struct Props { children: Children, @@ -768,10 +748,10 @@ mod layout_tests { } } } + let mut trun = TestRunner::new(); - let layout = TestLayout { - name: "13", - node: html! { + trun.step("Component with children") + .render(html! { if true { { "hello" } @@ -781,10 +761,8 @@ mod layout_tests { { "world" } } - }, - expected: "", - }; - - diff_layouts(vec![layout]); + }) + .await + .assert_inner_html(""); } } diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index 1d41e5dd0a2..25cc7298fee 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -512,72 +512,66 @@ mod layout_tests { use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; use crate::html; - use crate::tests::layout_tests::{diff_layouts, TestLayout}; + use crate::tests::{TestCase, TestRunner}; wasm_bindgen_test_configure!(run_in_browser); #[test] - fn diff() { - let layout1 = TestLayout { - name: "1", - node: html! { - <> - {"a"} - {"b"} - <> - {"c"} - {"d"} - - {"e"} - - }, - expected: "abcde", - }; + async fn diff() { + let mut trun = TestRunner::new(); - let layout2 = TestLayout { - name: "2", - node: html! { + trun.render(html! { + <> + {"a"} + {"b"} <> - {"a"} - {"b"} - <> - {"e"} - {"f"} - - }, - expected: "abef", - }; - - let layout3 = TestLayout { - name: "3", - node: html! { - <> - {"a"} - <> - {"b"} - {"e"} + {"c"} + {"d"} - }, - expected: "abe", - }; - - let layout4 = TestLayout { - name: "4", - node: html! { + {"e"} + + }) + .await + .assert_inner_html("abcde"); + + trun.render(html! { + <> + {"a"} + {"b"} + <> + {"e"} + {"f"} + + }) + .await + .assert_inner_html("abef"); + + trun.render(html! { + <> + {"a"} + <> + {"b"} + {"e"} + + }) + .await + .assert_inner_html("abe"); + + trun.render(html! { + <> + {"a"} <> - {"a"} - <> - {"c"} - {"d"} - - {"b"} - {"e"} + {"c"} + {"d"} - }, - expected: "acdbe", - }; - - diff_layouts(vec![layout1, layout2, layout3, layout4]); + {"b"} + {"e"} + + }) + .await + .assert_inner_html("acdbe"); + + trun.run_replayable_tests().await; } } @@ -589,10 +583,9 @@ mod layout_tests_keys { use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; use web_sys::Node; - use crate::tests::layout_tests::{diff_layouts, TestLayout}; + use crate::tests::{TestCase, TestRunner}; use crate::virtual_dom::VNode; use crate::{html, Children, Component, Context, Html, Properties}; - wasm_bindgen_test_configure!(run_in_browser); struct Comp {} @@ -646,13 +639,12 @@ mod layout_tests_keys { } #[test] - fn diff() { - let mut layouts = vec![]; + async fn diff() { + let mut trun = TestRunner::new(); let vref_node: Node = gloo_utils::document().create_element("i").unwrap().into(); - layouts.push(TestLayout { - name: "All VNode types as children", - node: html! { + trun.step("All VNode types as children") + .render(html! { <> {"a"} @@ -665,782 +657,726 @@ mod layout_tests_keys { {VNode::VRef(vref_node)} - }, - expected: "acd

0

foobar", - }); - - layouts.extend(vec![ - TestLayout { - name: "Inserting into VList first child - before", - node: html! { - <> - - - -

- - }, - expected: "

", - }, - TestLayout { - name: "Inserting into VList first child - after", - node: html! { - <> - - - - -

- - }, - expected: "

", - }, - ]); + }) + .await + .assert_inner_html("acd

0

foobar"); - layouts.extend(vec![ - TestLayout { - name: "No matches - before", - node: html! { - <> - - - - }, - expected: "", - }, - TestLayout { - name: "No matches - after", - node: html! { - <> - -

- - }, - expected: "

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Append - before", - node: html! { - <> - - - - }, - expected: "", - }, - TestLayout { - name: "Append - after", - node: html! { - <> - - -

- - }, - expected: "

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Prepend - before", - node: html! { - <> - - - - }, - expected: "", - }, - TestLayout { - name: "Prepend - after", - node: html! { - <> -

- - - - }, - expected: "

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Delete first - before", - node: html! { - <> - - -

- - }, - expected: "

", - }, - TestLayout { - name: "Delete first - after", - node: html! { - <> - -

- - }, - expected: "

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Delete last - before", - node: html! { - <> + let mut step = trun.step("Inserting into VList first child"); + step.step("before") + .render(html! { + <> + - -

- }, - expected: "

", - }, - TestLayout { - name: "Delete last - after", - node: html! { - <> +

+ + }) + .await + .assert_inner_html("

"); + step.step("after") + .render(html! { + <> + - }, - expected: "", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Delete last and change node type - before", - node: html! { - <> - - +

+ + }) + .await + .assert_inner_html("

"); + step.finish(); + + let mut step = trun.step("No matches"); + step.step("before") + .render(html! { + <> + + + + }) + .await + .assert_inner_html(""); + step.step("after") + .render(html! { + <> + +

+ + }) + .await + .assert_inner_html("

"); + step.finish(); + + let mut step = trun.step("Append"); + step.step("before") + .render(html! { + <> + + + + }) + .await + .assert_inner_html(""); + step.step("after") + .render(html! { + <> + + +

+ + }) + .await + .assert_inner_html("

"); + step.finish(); + + let mut step = trun.step("Prepend"); + step.step("before") + .render(html! { + <> + + + + }) + .await + .assert_inner_html(""); + step.step("after") + .render(html! { + <> +

+ + + + }) + .await + .assert_inner_html("

"); + step.finish(); + + let mut step = trun.step("Delete first"); + step.step("before") + .render(html! { + <> + + +

+ + }) + .await + .assert_inner_html("

"); + step.step("after") + .render(html! { + <> + +

+ + }) + .await + .assert_inner_html("

"); + step.finish(); + + let mut step = trun.step("Delete last"); + step.step("before") + .render(html! { + <> + + +

+ + }) + .await + .assert_inner_html("

"); + step.step("after") + .render(html! { + <> + + + + }) + .await + .assert_inner_html(""); + step.finish(); + + let mut step = trun.step("Delete last and change node type"); + step.step("before") + .render(html! { + <> + + +

+ + }) + .await + .assert_inner_html("

"); + step.step("after") + .render(html! { + <> + + + + + }) + .await + .assert_inner_html(""); + step.finish(); + + let mut step = trun.step("Delete middle"); + step.step("before") + .render(html! { + <> + + +

+ + + }) + .await + .assert_inner_html("

"); + step.step("after") + .render(html! { + <> + + +

+ + + }) + .await + .assert_inner_html("

"); + step.finish(); + + let mut step = trun.step("Delete middle and change node type"); + step.step("before") + .render(html! { + <> + + +

+ + + }) + .await + .assert_inner_html("

"); + step.step("after") + .render(html! { + <> + + +

+ + + }) + .await + .assert_inner_html("

"); + step.finish(); + + let mut step = trun.step("Reverse"); + step.step("before") + .render(html! { + <> + + +

+ + + }) + .await + .assert_inner_html("

"); + step.step("after") + .render(html! { + <> + +

+ + + + }) + .await + .assert_inner_html("

"); + step.finish(); + + let mut step = trun.step("Reverse and change node type"); + step.step("before") + .render(html! { + <> + + + + + +

- }, - expected: "

", - }, - TestLayout { - name: "Delete last - after", - node: html! { - <> - - - - - }, - expected: "", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Delete middle - before", - node: html! { - <> - + + + }) + .await + .assert_inner_html("

"); + step.step("after") + .render(html! { + <> + +

+ + + + }) + .await + .assert_inner_html("

"); + step.finish(); + + let mut step = trun.step("Swap 1&2"); + step.step("before") + .render(html! { + <> + + +

+ + + + }) + .await + .assert_inner_html("

"); + step.step("after") + .render(html! { + <> + + +

+ + + + }) + .await + .assert_inner_html("

"); + step.finish(); + + let mut step = trun.step("Swap 1&2 and change node type"); + step.step("before") + .render(html! { + <> + + +

+ + + + }) + .await + .assert_inner_html("

"); + step.step("after") + .render(html! { + <> + + +

+ + + + }) + .await + .assert_inner_html("

"); + step.finish(); + + let mut step = trun.step("Fragment list"); + step.step("before") + .render(html! { + <> +

+ - }, - expected: "

", - }, - TestLayout { - name: "Delete middle - after", - node: html! { - <> - - -

- - - }, - expected: "

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Delete middle and change node type - before", - node: html! { - <> - +

- - }, - expected: "

", - }, - TestLayout { - name: "Delete middle and change node type- after", - node: html! { - <> - - -

- - - }, - expected: "

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Reverse - before", - node: html! { - <> - - -

- }, - expected: "

", - }, - TestLayout { - name: "Reverse - after", - node: html! { - <> - + + }) + .await + .assert_inner_html("

"); + step.step("after") + .render(html! { + <> + +

- - - - }, - expected: "

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Reverse and change node type - before", - node: html! { - <> - - - - - -

- - - - }, - expected: "

", - }, - TestLayout { - name: "Reverse and change node type - after", - node: html! { - <> - -

- - - - }, - expected: "

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Swap 1&2 - before", - node: html! { - <> - - -

- - - - }, - expected: "

", - }, - TestLayout { - name: "Swap 1&2 - after", - node: html! { - <> - - -

- - - - }, - expected: "

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Swap 1&2 and change node type - before", - node: html! { - <> - - -

- - - - }, - expected: "

", - }, - TestLayout { - name: "Swap 1&2 and change node type - after", - node: html! { - <> - - -

- - - - }, - expected: "

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "test - before", - node: html! { - <> - - -

- - - - - -

- - - - - }, - expected: "

", - }, - TestLayout { - name: "Swap 4&5 - after", - node: html! { - <> - - -

- - - - }, - expected: "

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Swap 4&5 - before", - node: html! { - <> - - -

- - - - }, - expected: "

", - }, - TestLayout { - name: "Swap 4&5 - after", - node: html! { - <> - - -

- - - - }, - expected: "

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Swap 1&5 - before", - node: html! { - <> - - -

- - - - }, - expected: "

", - }, - TestLayout { - name: "Swap 1&5 - after", - node: html! { - <> - - -

- - - - }, - expected: "

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Move 2 after 4 - before", - node: html! { - <> - - -

- - - - }, - expected: "

", - }, - TestLayout { - name: "Move 2 after 4 - after", - node: html! { - <> - -

- - - - - }, - expected: "

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Swap 1,2 <-> 3,4 - before", - node: html! { - <> - - -

- - - - }, - expected: "

", - }, - TestLayout { - name: "Swap 1,2 <-> 3,4 - after", - node: html! { - <> -

- - - - - - }, - expected: "

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Swap lists - before", - node: html! { - <> - - - - - - - - - - }, - expected: "", - }, - TestLayout { - name: "Swap lists - after", - node: html! { - <> - - - - - - - - - - }, - expected: "", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Swap lists with in-between - before", - node: html! { - <> - - - - -

- - - - - - }, - expected: "

", - }, - TestLayout { - name: "Swap lists with in-between - after", - node: html! { - <> - - - - -

- - - - - - }, - expected: "

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Insert VComp front - before", - node: html! { - <> - - - - }, - expected: "", - }, - TestLayout { - name: "Insert VComp front - after", - node: html! { - <> - - - - - }, - expected: "

0

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Insert VComp middle - before", - node: html! { - <> - - - - }, - expected: "", - }, - TestLayout { - name: "Insert VComp middle - after", - node: html! { - <> - - - - }, - expected: "

0

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Insert VComp back - before", - node: html! { - <> - - + + }) + .await + .assert_inner_html("

"); + step.finish(); + + let mut step = trun.step("Swap 4&5"); + step.step("before") + .render(html! { + <> + + +

+ + + + }) + .await + .assert_inner_html("

"); + step.step("after") + .render(html! { + <> + + +

+ + + + }) + .await + .assert_inner_html("

"); + step.finish(); + + let mut step = trun.step("Swap 1&5"); + step.step("before") + .render(html! { + <> + + +

+ + + + }) + .await + .assert_inner_html("

"); + step.step("after") + .render(html! { + <> + + +

+ + + + }) + .await + .assert_inner_html("

"); + step.finish(); + + let mut step = trun.step("Move 2 after 4"); + step.step("before") + .render(html! { + <> + + +

+ + + + }) + .await + .assert_inner_html("

"); + step.step("after") + .render(html! { + <> + +

+ + + + + }) + .await + .assert_inner_html("

"); + step.finish(); + + let mut step = trun.step("Swap 1,2 <-> 3,4"); + step.step("before") + .render(html! { + <> + + +

+ + + + }) + .await + .assert_inner_html("

"); + step.step("after") + .render(html! { + <> +

+ + + + + + }) + .await + .assert_inner_html("

"); + step.finish(); + + let mut step = trun.step("Swap lists"); + step.step("before") + .render(html! { + <> + + + - }, - expected: "", - }, - TestLayout { - name: "Insert VComp back - after", - node: html! { - <> - - - + + + - }, - expected: "

0

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Reverse VComp children - before", - node: html! { - <> - - - + + }) + .await + .assert_inner_html(""); + step.step("after") + .render(html! { + <> + + + - }, - expected: "

1

2

3

", - }, - TestLayout { - name: "Reverse VComp children - after", - node: html! { - <> - - - + + + - }, - expected: "

3

2

1

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Reverse VComp children with children - before", - node: html! { - <> -

{"11"}

{"12"}

-

{"21"}

{"22"}

-

{"31"}

{"32"}

+ + }) + .await + .assert_inner_html(""); + step.finish(); + + let mut step = trun.step("Swap lists with in-between"); + step.step("before") + .render(html! { + <> + + + - }, - expected: "

11

12

21

22

31

32

", - }, - TestLayout { - name: "Reverse VComp children with children - after", - node: html! { - <> -

{"31"}

{"32"}

-

{"21"}

{"22"}

-

{"11"}

{"12"}

+

+ + + - }, - expected: "

31

32

21

22

11

12

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Complex component update - before", - node: html! { - - - - - }, - expected: "

1

2

", - }, - TestLayout { - name: "Complex component update - after", - node: html! { - - - - - -

{"2"}

-
-
- }, - expected: "

1

2

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Reorder VComp children with children - before", - node: html! { - <> -

{"1"}

-

{"3"}

-

{"5"}

-

{"2"}

-

{"4"}

-

{"6"}

+ + }) + .await + .assert_inner_html("

"); + step.step("after") + .render(html! { + <> + + + - }, - expected: "

1

3

5

2

4

6

", - }, - TestLayout { - name: "Reorder VComp children with children - after", - node: html! { - <> - - - - - - +

+ + + - }, - expected: "

6

5

4

3

2

1

", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Replace and reorder components - before", - node: html! { - -

{"1"}

-

{"2"}

-

{"3"}

+ + }) + .await + .assert_inner_html("

"); + step.finish(); + + let mut step = trun.step("Insert VComp front"); + step.step("before") + .render(html! { + <> + + + + }) + .await + .assert_inner_html(""); + step.step("after") + .render(html! { + <> + + + + + }) + .await + .assert_inner_html("

0

"); + step.finish(); + + let mut step = trun.step("Insert VComp middle"); + step.step("before") + .render(html! { + <> + + + + }) + .await + .assert_inner_html(""); + step.step("after") + .render(html! { + <> + + + + + }) + .await + .assert_inner_html("

0

"); + step.finish(); + + let mut step = trun.step("Insert VComp back"); + step.step("before") + .render(html! { + <> + + + + }) + .await + .assert_inner_html(""); + step.step("after") + .render(html! { + <> + + + + + }) + .await + .assert_inner_html("

0

"); + step.finish(); + + let mut step = trun.step("Reverse VComp children"); + step.step("before") + .render(html! { + <> + + + + + }) + .await + .assert_inner_html("

1

2

3

"); + step.step("after") + .render(html! { + <> + + + + + }) + .await + .assert_inner_html("

3

2

1

"); + step.finish(); + + let mut step = trun.step("Reverse VComp children with children"); + step.step("before") + .render(html! { + <> +

{"11"}

{"12"}

+

{"21"}

{"22"}

+

{"31"}

{"32"}

+ + }) + .await + .assert_inner_html("

11

12

21

22

31

32

"); + step.step("after") + .render(html! { + <> +

{"31"}

{"32"}

+

{"21"}

{"22"}

+

{"11"}

{"12"}

+ + }) + .await + .assert_inner_html("

31

32

21

22

11

12

"); + step.finish(); + + let mut step = trun.step("Complex component update"); + step.step("before") + .render(html! { + + + + + }) + .await + .assert_inner_html("

1

2

"); + step.step("after") + .render(html! { + + + - }, - expected: "

1

2

3

", - }, - TestLayout { - name: "Replace and reorder components - after", - node: html! { - - - - + +

{"2"}

- }, - expected: "

3

2

1

", - }, - ]); - - diff_layouts(layouts); +
+ }) + .await + .assert_inner_html("

1

2

"); + step.finish(); + + let mut step = trun.step("Reorder VComp children with children"); + step.step("before") + .render(html! { + <> +

{"1"}

+

{"3"}

+

{"5"}

+

{"2"}

+

{"4"}

+

{"6"}

+ + }) + .await + .assert_inner_html("

1

3

5

2

4

6

"); + step.step("after") + .render(html! { + <> + + + + + + + + }) + .await + .assert_inner_html("

6

5

4

3

2

1

"); + step.finish(); + + let mut step = trun.step("Replace and reorder components"); + step.step("before") + .render(html! { + +

{"1"}

+

{"2"}

+

{"3"}

+
+ }) + .await + .assert_inner_html("

1

2

3

"); + step.step("after") + .render(html! { + + + + + + }) + .await + .assert_inner_html("

3

2

1

"); + step.finish(); + + trun.run_replayable_tests().await; } } diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index 104e803a494..31377669f71 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -297,28 +297,22 @@ mod layout_tests { use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; use super::*; - use crate::tests::layout_tests::{diff_layouts, TestLayout}; + use crate::tests::{TestCase, TestRunner}; wasm_bindgen_test_configure!(run_in_browser); #[test] - fn diff() { + async fn diff() { + let mut trun = TestRunner::new(); let document = gloo_utils::document(); - let vref_node_1 = VNode::VRef(document.create_element("i").unwrap().into()); - let vref_node_2 = VNode::VRef(document.create_element("b").unwrap().into()); - let layout1 = TestLayout { - name: "1", - node: vref_node_1, - expected: "", - }; + trun.render(VNode::VRef(document.create_element("i").unwrap().into())) + .await + .assert_inner_html(""); + trun.render(VNode::VRef(document.create_element("b").unwrap().into())) + .await + .assert_inner_html(""); - let layout2 = TestLayout { - name: "2", - node: vref_node_2, - expected: "", - }; - - diff_layouts(vec![layout1, layout2]); + trun.run_replayable_tests().await; } } diff --git a/packages/yew/src/dom_bundle/bportal.rs b/packages/yew/src/dom_bundle/bportal.rs index 669ff6ad2af..59f11ba9272 100644 --- a/packages/yew/src/dom_bundle/bportal.rs +++ b/packages/yew/src/dom_bundle/bportal.rs @@ -121,29 +121,25 @@ impl BPortal { #[cfg(target_arch = "wasm32")] #[cfg(test)] mod layout_tests { - extern crate self as yew; - use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - use yew::virtual_dom::VPortal; use crate::html; - use crate::tests::layout_tests::{diff_layouts, TestLayout}; - use crate::virtual_dom::VNode; + use crate::tests::{TestCase, TestRunner}; + use crate::virtual_dom::{VNode, VPortal}; wasm_bindgen_test_configure!(run_in_browser); #[test] - fn diff() { - let mut layouts = vec![]; + async fn diff() { + let mut trun = TestRunner::new(); let first_target = gloo_utils::document().create_element("i").unwrap(); let second_target = gloo_utils::document().create_element("o").unwrap(); let target_with_child = gloo_utils::document().create_element("i").unwrap(); let target_child = gloo_utils::document().create_element("s").unwrap(); target_with_child.append_child(&target_child).unwrap(); - layouts.push(TestLayout { - name: "Portal - first target", - node: html! { + trun.step("Portal - first target") + .render(html! {
{VNode::VRef(first_target.clone().into())} {VNode::VRef(second_target.clone().into())} @@ -153,12 +149,11 @@ mod layout_tests { ))} {"AFTER"}
- }, - expected: "
PORTALAFTER
", - }); - layouts.push(TestLayout { - name: "Portal - second target", - node: html! { + }) + .await + .assert_inner_html("
PORTALAFTER
"); + trun.step("Portal - second target") + .render(html! {
{VNode::VRef(first_target.clone().into())} {VNode::VRef(second_target.clone().into())} @@ -168,24 +163,22 @@ mod layout_tests { ))} {"AFTER"}
- }, - expected: "
PORTALAFTER
", - }); - layouts.push(TestLayout { - name: "Portal - replaced by text", - node: html! { + }) + .await + .assert_inner_html("
PORTALAFTER
"); + trun.step("Portal - replaced by text") + .render(html! {
{VNode::VRef(first_target.clone().into())} {VNode::VRef(second_target.clone().into())} {"FOO"} {"AFTER"}
- }, - expected: "
FOOAFTER
", - }); - layouts.push(TestLayout { - name: "Portal - next sibling", - node: html! { + }) + .await + .assert_inner_html("
FOOAFTER
"); + trun.step("Portal - next sibling") + .render(html! {
{VNode::VRef(target_with_child.clone().into())} {VNode::VPortal(VPortal::new_before( @@ -194,10 +187,10 @@ mod layout_tests { Some(target_child.clone().into()), ))}
- }, - expected: "
PORTAL
", - }); + }) + .await + .assert_inner_html("
PORTAL
"); - diff_layouts(layouts) + trun.run_replayable_tests().await; } } diff --git a/packages/yew/src/dom_bundle/btag/listeners.rs b/packages/yew/src/dom_bundle/btag/listeners.rs index 55d708af39b..5ab81920456 100644 --- a/packages/yew/src/dom_bundle/btag/listeners.rs +++ b/packages/yew/src/dom_bundle/btag/listeners.rs @@ -199,20 +199,21 @@ impl Registry { #[cfg(target_arch = "wasm32")] #[cfg(test)] mod tests { - use std::marker::PhantomData; - use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - use web_sys::{Event, EventInit, HtmlElement, MouseEvent}; + use web_sys::{ + Event, EventInit, HtmlElement, HtmlInputElement, MouseEvent, ShadowRootInit, ShadowRootMode, + }; wasm_bindgen_test_configure!(run_in_browser); use gloo_utils::document; use wasm_bindgen::JsCast; - use yew::Callback; - use crate::html::TargetCast; + use crate::html::{Scope, TargetCast}; + use crate::tests::{TestCase, TestRunner}; use crate::virtual_dom::VNode; use crate::{ - create_portal, html, scheduler, AppHandle, Component, Context, Html, NodeRef, Properties, + create_portal, function_component, hook, html, html_nested, scheduler, use_context, + Callback, Children, Component, Context, ContextProvider, Html, NodeRef, Properties, }; #[derive(Clone)] @@ -222,44 +223,47 @@ mod tests { SetText(String), } - #[derive(Default)] - struct State { + #[derive(Clone)] + struct TestContext { stop_listening: bool, action: u32, text: String, + state_ref: NodeRef, + link: Scope, } - #[derive(Default, PartialEq, Properties)] - struct MixinProps { - state_ref: NodeRef, - wrapped: M, + impl PartialEq for TestContext { + fn eq(&self, other: &Self) -> bool { + self.stop_listening == other.stop_listening + && self.action == other.action + && self.text == other.text + && self.state_ref == other.state_ref + } } - trait Mixin: Properties + Sized { - fn view(ctx: &Context, state: &State) -> Html - where - C: Component>; + #[derive(PartialEq, Properties)] + struct TestProps { + state_ref: NodeRef, + children: Children, } - struct Comp - where - M: Mixin + 'static, - { - state: State, - pd: PhantomData, + struct Comp { + state: TestContext, } - impl Component for Comp - where - M: Mixin + Properties + 'static, - { + impl Component for Comp { type Message = Message; - type Properties = MixinProps; + type Properties = TestProps; - fn create(_: &Context) -> Self { + fn create(ctx: &Context) -> Self { Comp { - state: Default::default(), - pd: PhantomData, + state: TestContext { + stop_listening: false, + action: 0, + text: "".to_string(), + state_ref: ctx.props().state_ref.clone(), + link: ctx.link().clone(), + }, } } @@ -279,10 +283,19 @@ mod tests { } fn view(&self, ctx: &Context) -> crate::Html { - M::view(ctx, &self.state) + html! { + context={self.state.clone()}> + {for ctx.props().children.iter()} + > + } } } + #[hook] + fn use_test_context() -> TestContext { + use_context::().unwrap() + } + #[track_caller] fn assert_count(el: &NodeRef, count: isize) { let text = el @@ -298,63 +311,42 @@ mod tests { scheduler::start_now(); } - fn get_el_by_selector(selector: &str) -> web_sys::HtmlElement { - document() - .query_selector(selector) - .unwrap() - .unwrap() - .dyn_into::() - .unwrap() - } - - fn init() -> (AppHandle>, NodeRef) - where - M: Mixin + Properties + Default, - { - // Remove any existing elements - let body = document().body().unwrap(); - while let Some(child) = body.query_selector("div#testroot").unwrap() { - body.remove_child(&child).unwrap(); - } - - let root = document().create_element("div").unwrap(); - root.set_id("testroot"); - body.append_child(&root).unwrap(); - let props = as Component>::Properties::default(); - let el_ref = props.state_ref.clone(); - let app = crate::Renderer::>::with_root_and_props(root, props).render(); - scheduler::start_now(); + #[track_caller] + async fn init(inner: Html) -> (Scope, NodeRef) { + let mut runner = TestRunner::new(); + let el = NodeRef::default(); + let (_, link) = runner + .render_app(html_nested! { + + {inner} + + }) + .await; - (app, el_ref) + (link, el) } #[test] - fn synchronous() { - #[derive(Default, PartialEq, Properties)] - struct Synchronous; - - impl Mixin for Synchronous { - fn view(ctx: &Context, state: &State) -> Html - where - C: Component>, - { - let onclick = ctx.link().callback(|_| Message::Action); - - if state.stop_listening { - html! { - {state.action} - } - } else { - html! { - - {state.action} - - } + async fn synchronous() { + #[function_component] + fn Synchronous() -> Html { + let state = use_test_context(); + let onclick = state.link.callback(|_| Message::Action); + + if state.stop_listening { + html! { + {state.action} + } + } else { + html! { + + {state.action} + } } } - let (link, el) = init::(); + let (link, el) = init(html! { }).await; assert_count(&el, 0); @@ -373,31 +365,25 @@ mod tests { #[test] async fn non_bubbling_event() { - #[derive(Default, PartialEq, Properties)] - struct NonBubbling; - - impl Mixin for NonBubbling { - fn view(ctx: &Context, state: &State) -> Html - where - C: Component>, - { - let link = ctx.link().clone(); - let onblur = Callback::from(move |_| { - link.send_message(Message::Action); - scheduler::start_now(); - }); - html! { - - } + #[function_component] + fn NonBubbling() -> Html { + let state = use_test_context(); + let link = state.link.clone(); + let onblur = Callback::from(move |_| { + link.send_message(Message::Action); + scheduler::start_now(); + }); + html! { + } } - let (_, el) = init::(); + let (_, el) = init(html! { }).await; assert_count(&el, 0); @@ -418,37 +404,30 @@ mod tests { } #[test] - fn bubbling() { - #[derive(Default, PartialEq, Properties)] - struct Bubbling; - - impl Mixin for Bubbling { - fn view(ctx: &Context, state: &State) -> Html - where - C: Component>, - { - if state.stop_listening { - html! { - - } - } else { - let cb = ctx.link().callback(|_| Message::Action); - html! { - - } + async fn bubbling() { + #[function_component] + fn Bubbling() -> Html { + let state = use_test_context(); + if state.stop_listening { + html! { + + } + } else { + let cb = state.link.callback(|_| Message::Action); + html! { + } } } - - let (link, el) = init::(); + let (link, el) = init(html! { }).await; assert_count(&el, 0); click(&el); @@ -463,32 +442,26 @@ mod tests { } #[test] - fn cancel_bubbling() { - #[derive(Default, PartialEq, Properties)] - struct CancelBubbling; - - impl Mixin for CancelBubbling { - fn view(ctx: &Context, state: &State) -> Html - where - C: Component>, - { - let onclick = ctx.link().callback(|_| Message::Action); - let onclick2 = ctx.link().callback(|e: MouseEvent| { - e.stop_propagation(); - Message::Action - }); - - html! { - - } + async fn cancel_bubbling() { + #[function_component] + fn CancelBubbling() -> Html { + let state = use_test_context(); + let onclick = state.link.callback(|_| Message::Action); + let onclick2 = state.link.callback(|e: MouseEvent| { + e.stop_propagation(); + Message::Action + }); + + html! { + } } - let (_, el) = init::(); + let (_, el) = init(html! { }).await; assert_count(&el, 0); click(&el); @@ -498,36 +471,30 @@ mod tests { } #[test] - fn cancel_bubbling_nested() { + async fn cancel_bubbling_nested() { // Here an event is being delivered to a DOM node which does // _not_ have a listener but which is contained within an // element that does and which cancels the bubble. - #[derive(Default, PartialEq, Properties)] - struct CancelBubbling; - - impl Mixin for CancelBubbling { - fn view(ctx: &Context, state: &State) -> Html - where - C: Component>, - { - let onclick = ctx.link().callback(|_| Message::Action); - let onclick2 = ctx.link().callback(|e: MouseEvent| { - e.stop_propagation(); - Message::Action - }); - html! { -
- + #[function_component] + fn CancelBubbling() -> Html { + let state = use_test_context(); + let onclick = state.link.callback(|_| Message::Action); + let onclick2 = state.link.callback(|e: MouseEvent| { + e.stop_propagation(); + Message::Action + }); + html! { + } } - let (_, el) = init::(); + let (_, el) = init(html! { }).await; assert_count(&el, 0); click(&el); @@ -540,41 +507,32 @@ mod tests { /// in a portal. It should bubble through the portal and reach the containing /// element. #[test] - fn portal_bubbling() { + async fn portal_bubbling() { #[derive(PartialEq, Properties)] - struct PortalBubbling { + struct PortalBubblingProps { host: web_sys::Element, } - impl Default for PortalBubbling { - fn default() -> Self { - let host = document().create_element("div").unwrap(); - PortalBubbling { host } - } - } - - impl Mixin for PortalBubbling { - fn view(ctx: &Context, state: &State) -> Html - where - C: Component>, - { - let portal_target = ctx.props().wrapped.host.clone(); - let onclick = ctx.link().callback(|_| Message::Action); - html! { - <> -
- {create_portal(html! { - - {state.action} - - }, portal_target.clone())} -
- {VNode::VRef(portal_target.into())} - - } + #[function_component] + fn PortalBubbling(PortalBubblingProps { host }: &PortalBubblingProps) -> Html { + let state = use_test_context(); + let portal_target = host.clone(); + let onclick = state.link.callback(|_| Message::Action); + html! { + <> +
+ {create_portal(html! { + + {state.action} + + }, portal_target.clone())} +
+ {VNode::VRef(portal_target.into())} + } } - let (_, el) = init::(); + let host = document().create_element("div").unwrap(); + let (_, el) = init(html! { }).await; assert_count(&el, 0); click(&el); @@ -584,98 +542,86 @@ mod tests { /// Here an event is being from inside a shadow root. It should only be caught exactly once on /// each handler #[test] - fn open_shadow_dom_bubbling() { - use web_sys::{ShadowRootInit, ShadowRootMode}; + async fn open_shadow_dom_bubbling() { #[derive(PartialEq, Properties)] - struct OpenShadowDom { + struct OpenShadowDomProps { host: web_sys::Element, inner_root: web_sys::Element, } - impl Default for OpenShadowDom { - fn default() -> Self { - let host = document().create_element("div").unwrap(); - let inner_root = document().create_element("div").unwrap(); - let shadow = host - .attach_shadow(&ShadowRootInit::new(ShadowRootMode::Open)) - .unwrap(); - shadow.append_child(&inner_root).unwrap(); - OpenShadowDom { host, inner_root } - } - } - impl Mixin for OpenShadowDom { - fn view(ctx: &Context, state: &State) -> Html - where - C: Component>, - { - let onclick = ctx.link().callback(|_| Message::Action); - let mixin = &ctx.props().wrapped; - html! { -
-
- {create_portal(html! { - - {state.action} - - }, mixin.inner_root.clone())} -
- {VNode::VRef(mixin.host.clone().into())} + #[function_component] + fn OpenShadowDom(OpenShadowDomProps { host, inner_root }: &OpenShadowDomProps) -> Html { + let state = use_test_context(); + let onclick = state.link.callback(|_| Message::Action); + html! { +
+
+ {create_portal(html! { + + {state.action} + + }, inner_root.clone())}
- } + {VNode::VRef(host.clone().into())} +
} } - let (_, el) = init::(); + + let host = document().create_element("div").unwrap(); + let inner_root = document().create_element("div").unwrap(); + let shadow = host + .attach_shadow(&ShadowRootInit::new(ShadowRootMode::Open)) + .unwrap(); + shadow.append_child(&inner_root).unwrap(); + let (_, el) = init(html! { }).await; assert_count(&el, 0); click(&el); assert_count(&el, 2); // Once caught per handler } - fn test_input_listener(make_event: impl Fn() -> E) + async fn test_input_listener(make_event: impl Fn() -> E) where E: Into + std::fmt::Debug, { - #[derive(Default, PartialEq, Properties)] - struct Input; - - impl Mixin for Input { - fn view(ctx: &Context, state: &State) -> Html - where - C: Component>, - { - if state.stop_listening { - html! { -
- -

{state.text.clone()}

-
- } - } else { - let onchange = ctx.link().callback(|e: web_sys::Event| { - let el: web_sys::HtmlInputElement = e.target_unchecked_into(); - Message::SetText(el.value()) - }); - let oninput = ctx.link().callback(|e: web_sys::InputEvent| { - let el: web_sys::HtmlInputElement = e.target_unchecked_into(); - Message::SetText(el.value()) - }); - - html! { -
- -

{state.text.clone()}

-
- } + #[derive(PartialEq, Properties)] + struct InputProps { + input_el: NodeRef, + } + #[function_component] + fn Input(InputProps { input_el }: &InputProps) -> Html { + let state = use_test_context(); + if state.stop_listening { + html! { +
+ +

{state.text.clone()}

+
+ } + } else { + let onchange = state.link.callback(|e: web_sys::Event| { + let el: HtmlInputElement = e.target_unchecked_into(); + Message::SetText(el.value()) + }); + let oninput = state.link.callback(|e: web_sys::InputEvent| { + let el: HtmlInputElement = e.target_unchecked_into(); + Message::SetText(el.value()) + }); + + html! { +
+ +

{state.text.clone()}

+
} } } - let (link, state_ref) = init::(); - let input_el = get_el_by_selector("input") - .dyn_into::() - .unwrap(); + let input_el = NodeRef::default(); + let (link, state_ref) = init(html! { }).await; assert_eq!(&state_ref.get().unwrap().text_content().unwrap(), ""); for mut s in ["foo", "bar", "baz"].iter() { + let input_el = input_el.cast::().unwrap(); input_el.set_value(s); if s == &"baz" { link.send_message(Message::StopListening); @@ -690,7 +636,7 @@ mod tests { } #[test] - fn oninput() { + async fn oninput() { test_input_listener(|| { web_sys::InputEvent::new_with_event_init_dict( "input", @@ -698,10 +644,11 @@ mod tests { ) .unwrap() }) + .await } #[test] - fn onchange() { + async fn onchange() { test_input_listener(|| { web_sys::Event::new_with_event_init_dict( "change", @@ -709,5 +656,6 @@ mod tests { ) .unwrap() }) + .await } } diff --git a/packages/yew/src/dom_bundle/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs index d96bb364aeb..bc5ad846d51 100644 --- a/packages/yew/src/dom_bundle/btag/mod.rs +++ b/packages/yew/src/dom_bundle/btag/mod.rs @@ -383,32 +383,10 @@ mod feat_hydration { } } -#[cfg(target_arch = "wasm32")] #[cfg(test)] mod tests { - use gloo_utils::document; - use wasm_bindgen::JsCast; - use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - use web_sys::HtmlInputElement as InputElement; - - use super::*; - use crate::dom_bundle::{BNode, Reconcilable, ReconcileTarget}; - use crate::html::AnyScope; - use crate::virtual_dom::vtag::{HTML_NAMESPACE, SVG_NAMESPACE}; use crate::virtual_dom::{AttrValue, VNode, VTag}; - use crate::{html, Html, NodeRef}; - - wasm_bindgen_test_configure!(run_in_browser); - - fn setup_parent() -> (BSubtree, AnyScope, Element) { - let scope = AnyScope::test(); - let parent = document().create_element("div").unwrap(); - let root = BSubtree::create_root(&parent); - - document().body().unwrap().append_child(&parent).unwrap(); - - (root, scope, parent) - } + use crate::{html, NodeRef}; #[test] fn it_compares_tags() { @@ -559,13 +537,6 @@ mod tests { panic!("should be vtag"); } - fn assert_btag_ref(node: &BNode) -> &BTag { - if let BNode::Tag(vtag) = node { - return vtag; - } - panic!("should be btag"); - } - fn assert_vtag_ref(node: &VNode) -> &VTag { if let VNode::VTag(vtag) = node { return vtag; @@ -573,43 +544,21 @@ mod tests { panic!("should be vtag"); } - fn assert_btag_mut(node: &mut BNode) -> &mut BTag { - if let BNode::Tag(btag) = node { - return btag; - } - panic!("should be btag"); - } - - fn assert_namespace(vtag: &BTag, namespace: &'static str) { - assert_eq!(vtag.reference().namespace_uri().unwrap(), namespace); - } - #[test] - fn supports_svg() { - let (root, scope, parent) = setup_parent(); - let document = web_sys::window().unwrap().document().unwrap(); - - let namespace = SVG_NAMESPACE; - let namespace = Some(namespace); - let svg_el = document.create_element_ns(namespace, "svg").unwrap(); - - let g_node = html! { }; + fn html_generates_vtags() { + let node_ref = NodeRef::default(); + let elem = html! {
}; + assert_vtag(elem); + let expected = "not_changed_value"; + let elem = html! { }; + assert_vtag(elem); let path_node = html! { }; let svg_node = html! { {path_node} }; - - let svg_tag = assert_vtag(svg_node); - let (_, svg_tag) = svg_tag.attach(&root, &scope, &parent, NodeRef::default()); - assert_namespace(&svg_tag, SVG_NAMESPACE); - let path_tag = assert_btag_ref(svg_tag.children().get(0).unwrap()); - assert_namespace(path_tag, SVG_NAMESPACE); - - let g_tag = assert_vtag(g_node.clone()); - let (_, g_tag) = g_tag.attach(&root, &scope, &parent, NodeRef::default()); - assert_namespace(&g_tag, HTML_NAMESPACE); - - let g_tag = assert_vtag(g_node); - let (_, g_tag) = g_tag.attach(&root, &scope, &svg_el, NodeRef::default()); - assert_namespace(&g_tag, SVG_NAMESPACE); + assert_vtag(svg_node); + let g_node = html! { }; + assert_vtag(g_node); + let next_elem = html! { }; + assert_vtag(next_elem); } #[test] @@ -702,124 +651,6 @@ mod tests { } } - #[test] - fn it_does_not_set_missing_class_name() { - let (root, scope, parent) = setup_parent(); - - let elem = html! {
}; - let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default()); - let vtag = assert_btag_mut(&mut elem); - // test if the className has not been set - assert!(!vtag.reference().has_attribute("class")); - } - - fn test_set_class_name(gen_html: impl FnOnce() -> Html) { - let (root, scope, parent) = setup_parent(); - - let elem = gen_html(); - let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default()); - let vtag = assert_btag_mut(&mut elem); - // test if the className has been set - assert!(vtag.reference().has_attribute("class")); - } - - #[test] - fn it_sets_class_name_static() { - test_set_class_name(|| html! {
}); - } - - #[test] - fn it_sets_class_name_dynamic() { - test_set_class_name(|| html! {
}); - } - - #[test] - fn controlled_input_synced() { - let (root, scope, parent) = setup_parent(); - - let expected = "not_changed_value"; - - // Initial state - let elem = html! { }; - let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default()); - let vtag = assert_btag_ref(&elem); - - // User input - let input_ref = &vtag.reference(); - let input = input_ref.dyn_ref::(); - input.unwrap().set_value("User input"); - - let next_elem = html! { }; - let elem_vtag = assert_vtag(next_elem); - - // Sync happens here - elem_vtag.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut elem); - let vtag = assert_btag_ref(&elem); - - // Get new current value of the input element - let input_ref = &vtag.reference(); - let input = input_ref.dyn_ref::().unwrap(); - - let current_value = input.value(); - - // check whether not changed virtual dom value has been set to the input element - assert_eq!(current_value, expected); - } - - #[test] - fn uncontrolled_input_unsynced() { - let (root, scope, parent) = setup_parent(); - - // Initial state - let elem = html! { }; - let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default()); - let vtag = assert_btag_ref(&elem); - - // User input - let input_ref = &vtag.reference(); - let input = input_ref.dyn_ref::(); - input.unwrap().set_value("User input"); - - let next_elem = html! { }; - let elem_vtag = assert_vtag(next_elem); - - // Value should not be refreshed - elem_vtag.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut elem); - let vtag = assert_btag_ref(&elem); - - // Get user value of the input element - let input_ref = &vtag.reference(); - let input = input_ref.dyn_ref::().unwrap(); - - let current_value = input.value(); - - // check whether not changed virtual dom value has been set to the input element - assert_eq!(current_value, "User input"); - - // Need to remove the element to clean up the dirty state of the DOM. Failing this causes - // event listener tests to fail. - parent.remove(); - } - - #[test] - fn dynamic_tags_work() { - let (root, scope, parent) = setup_parent(); - - let elem = html! { <@{ - let mut builder = String::new(); - builder.push('a'); - builder - }/> }; - - let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default()); - let vtag = assert_btag_mut(&mut elem); - // make sure the new tag name is used internally - assert_eq!(vtag.tag(), "a"); - - // Element.tagName is always in the canonical upper-case form. - assert_eq!(vtag.reference().tag_name(), "A"); - } - #[test] fn dynamic_tags_handle_value_attribute() { let div_el = html! { @@ -862,218 +693,6 @@ mod tests { assert_eq!(vtag.tag(), "clipPath"); } - #[test] - fn reset_node_ref() { - let (root, scope, parent) = setup_parent(); - - let node_ref = NodeRef::default(); - let elem: VNode = html! {
}; - assert_vtag_ref(&elem); - let (_, elem) = elem.attach(&root, &scope, &parent, NodeRef::default()); - assert_eq!(node_ref.get(), parent.first_child()); - elem.detach(&root, &parent, false); - assert!(node_ref.get().is_none()); - } - - #[test] - fn vtag_reuse_should_reset_ancestors_node_ref() { - let (root, scope, parent) = setup_parent(); - - let node_ref_a = NodeRef::default(); - let elem_a = html! {
}; - let (_, mut elem) = elem_a.attach(&root, &scope, &parent, NodeRef::default()); - - // save the Node to check later that it has been reused. - let node_a = node_ref_a.get().unwrap(); - - let node_ref_b = NodeRef::default(); - let elem_b = html! {
}; - elem_b.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut elem); - - let node_b = node_ref_b.get().unwrap(); - - assert_eq!(node_a, node_b, "VTag should have reused the element"); - assert!( - node_ref_a.get().is_none(), - "node_ref_a should have been reset when the element was reused." - ); - } - - #[test] - fn vtag_should_not_touch_newly_bound_refs() { - let (root, scope, parent) = setup_parent(); - - let test_ref = NodeRef::default(); - let before = html! { - <> -
- - }; - let after = html! { - <> -
-
- - }; - // The point of this diff is to first render the "after" div and then detach the "before" - // div, while both should be bound to the same node ref - - let (_, mut elem) = before.attach(&root, &scope, &parent, NodeRef::default()); - after.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut elem); - - assert_eq!( - test_ref - .get() - .unwrap() - .dyn_ref::() - .unwrap() - .outer_html(), - "
" - ); - } - - // test for bug: https://github.com/yewstack/yew/pull/2653 - #[test] - fn test_index_map_attribute_diff() { - let (root, scope, parent) = setup_parent(); - - let test_ref = NodeRef::default(); - - // We want to test appy_diff with Attributes::IndexMap, so we - // need to create the VTag manually - - // Create
- let mut vtag = VTag::new("div"); - vtag.node_ref = test_ref.clone(); - vtag.add_attribute("disabled", "disabled"); - vtag.add_attribute("tabindex", "0"); - - let elem = VNode::VTag(Box::new(vtag)); - - let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default()); - - // Create
(removed first attribute "disabled") - let mut vtag = VTag::new("div"); - vtag.node_ref = test_ref.clone(); - vtag.add_attribute("tabindex", "0"); - let next_elem = VNode::VTag(Box::new(vtag)); - let elem_vtag = assert_vtag(next_elem); - - // Sync happens here - // this should remove the the "disabled" attribute - elem_vtag.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut elem); - - assert_eq!( - test_ref - .get() - .unwrap() - .dyn_ref::() - .unwrap() - .outer_html(), - "
" - ) - } -} - -#[cfg(target_arch = "wasm32")] -#[cfg(test)] -mod layout_tests { - extern crate self as yew; - - use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - - use crate::html; - use crate::tests::layout_tests::{diff_layouts, TestLayout}; - - wasm_bindgen_test_configure!(run_in_browser); - - #[test] - fn diff() { - let layout1 = TestLayout { - name: "1", - node: html! { -
    -
  • - {"a"} -
  • -
  • - {"b"} -
  • -
- }, - expected: "
  • a
  • b
", - }; - - let layout2 = TestLayout { - name: "2", - node: html! { -
    -
  • - {"a"} -
  • -
  • - {"b"} -
  • -
  • - {"d"} -
  • -
- }, - expected: "
  • a
  • b
  • d
", - }; - - let layout3 = TestLayout { - name: "3", - node: html! { -
    -
  • - {"a"} -
  • -
  • - {"b"} -
  • -
  • - {"c"} -
  • -
  • - {"d"} -
  • -
- }, - expected: "
  • a
  • b
  • c
  • d
", - }; - - let layout4 = TestLayout { - name: "4", - node: html! { -
    -
  • - <> - {"a"} - -
  • -
  • - {"b"} -
  • - {"c"} -
  • -
  • - {"d"} -
  • - -
- }, - expected: "
  • a
  • b
  • c
  • d
", - }; - - diff_layouts(vec![layout1, layout2, layout3, layout4]); - } -} - -#[cfg(test)] -mod tests_without_browser { - use crate::html; - #[test] fn html_if_bool() { assert_eq!( @@ -1246,3 +865,334 @@ mod tests_without_browser { ); } } + +#[cfg(target_arch = "wasm32")] +#[cfg(test)] +mod layout_tests { + extern crate self as yew; + + use gloo_utils::document; + use wasm_bindgen::JsCast; + use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + use web_sys::HtmlInputElement as InputElement; + + use super::BTag; + use crate::dom_bundle::BNode; + use crate::tests::{TestCase, TestRunner}; + use crate::virtual_dom::vtag::{HTML_NAMESPACE, SVG_NAMESPACE}; + use crate::virtual_dom::{VNode, VTag}; + use crate::{html, Html, NodeRef}; + + wasm_bindgen_test_configure!(run_in_browser); + + fn assert_btag_ref(node: &BNode) -> &BTag { + if let BNode::Tag(vtag) = node { + return vtag; + } + panic!("should be btag"); + } + + fn assert_namespace(vtag: &BTag, namespace: &'static str) { + assert_eq!(vtag.reference().namespace_uri().unwrap(), namespace); + } + + #[test] + async fn diff() { + let mut trun = TestRunner::new(); + + trun.render(html! { +
    +
  • + {"a"} +
  • +
  • + {"b"} +
  • +
+ }) + .await + .assert_inner_html("
  • a
  • b
"); + + trun.render(html! { +
    +
  • + {"a"} +
  • +
  • + {"b"} +
  • +
  • + {"d"} +
  • +
+ }) + .await + .assert_inner_html("
  • a
  • b
  • d
"); + + trun.render(html! { +
    +
  • + {"a"} +
  • +
  • + {"b"} +
  • +
  • + {"c"} +
  • +
  • + {"d"} +
  • +
+ }) + .await + .assert_inner_html("
  • a
  • b
  • c
  • d
"); + + trun.render(html! { +
    +
  • + <> + {"a"} + +
  • +
  • + {"b"} +
  • + {"c"} +
  • +
  • + {"d"} +
  • + +
+ }) + .await + .assert_inner_html("
  • a
  • b
  • c
  • d
"); + + trun.run_replayable_tests().await; + } + + #[test] + async fn supports_svg() { + let mut trun = TestRunner::new(); + + let svg_node = html! { }; + let mut render = trun.render(svg_node).await; + let svg_tag = assert_btag_ref(&render.bundle().as_node()); + assert_namespace(&svg_tag, SVG_NAMESPACE); + let path_tag = assert_btag_ref(svg_tag.children().get(0).unwrap()); + assert_namespace(path_tag, SVG_NAMESPACE); + drop(render); + + let g_node = html! { }; + let mut render = trun.render(g_node.clone()).await; + let g_tag = assert_btag_ref(&render.bundle().as_node()); + assert_namespace(&g_tag, HTML_NAMESPACE); + drop(render); + + let svg_parent = document() + .create_element_ns(Some(SVG_NAMESPACE), "svg") + .unwrap(); + let mut trun = TestRunner::new_in(svg_parent); + let mut render = trun.render(g_node).await; + let g_tag = assert_btag_ref(&render.bundle().as_node()); + assert_namespace(&g_tag, SVG_NAMESPACE); + } + + #[test] + async fn it_does_not_set_missing_class_name() { + let mut trun = TestRunner::new(); + + let elem = html! {
}; + let mut render = trun.render(elem).await; + let vtag = assert_btag_ref(&render.bundle().as_node()); + // test if the className has not been set + assert!(!vtag.reference().has_attribute("class")); + } + + async fn test_set_class_name(elem: Html) { + let mut trun = TestRunner::new(); + + let mut render = trun.render(elem).await; + let vtag = assert_btag_ref(&render.bundle().as_node()); + // test if the className has been set + assert!(vtag.reference().has_attribute("class")); + } + + #[test] + async fn it_sets_class_name_static() { + test_set_class_name(html! {
}).await; + } + + #[test] + async fn it_sets_class_name_dynamic() { + test_set_class_name(html! {
}).await; + } + + #[test] + async fn controlled_input_synced() { + let mut trun = TestRunner::new(); + let input_ref = NodeRef::default(); + let expected = "not_changed_value"; + + // Initial state + let elem = html! { }; + trun.render(elem).await; + input_ref + .cast::() + .unwrap() + .set_value("User input"); + + let elem = html! { }; + trun.render(elem).await; + // Get new current value of the input element + let current_value = input_ref.cast::().unwrap().value(); + // check whether not changed virtual dom value has been set to the input element + assert_eq!(current_value, expected); + } + + #[test] + async fn uncontrolled_input_unsynced() { + let mut trun = TestRunner::new(); + let input_ref = NodeRef::default(); + + // Initial state + let elem = html! { }; + trun.render(elem).await; + input_ref + .cast::() + .unwrap() + .set_value("User input"); + + let elem = html! { }; + trun.render(elem).await; + let current_value = input_ref.cast::().unwrap().value(); + // check whether not changed virtual dom value has been set to the input element + assert_eq!(current_value, "User input"); + } + + #[test] + async fn dynamic_tags_work() { + let mut trun = TestRunner::new(); + + let elem = html! { <@{ + let mut builder = String::new(); + builder.push('a'); + builder + } /> }; + let mut render = trun.render(elem).await; + let vtag = assert_btag_ref(&render.bundle().as_node()); + + // make sure the new tag name is used internally + assert_eq!(vtag.tag(), "a"); + + // Element.tagName is always in the canonical upper-case form. + assert_eq!(vtag.reference().tag_name(), "A"); + } + + #[test] + async fn reset_node_ref() { + let mut trun = TestRunner::new(); + let node_ref = NodeRef::default(); + + let elem = html! {
}; + let mut render = trun.render(elem).await; + assert_eq!(node_ref.get(), render.parent().first_child()); + drop(render); + + trun.render(Html::default()).await; + assert!(node_ref.get().is_none()); + } + + #[test] + async fn vtag_reuse_should_reset_ancestors_node_ref() { + let mut trun = TestRunner::new(); + let node_ref_a = NodeRef::default(); + let node_ref_b = NodeRef::default(); + + let elem_a = html! {
}; + trun.render(elem_a).await; + + // save the Node to check later that it has been reused. + let node_a = node_ref_a.get().unwrap(); + + let elem_b = html! {
}; + trun.render(elem_b).await; + + let node_b = node_ref_b.get().unwrap(); + + assert_eq!(node_a, node_b, "VTag should have reused the element"); + assert!( + node_ref_a.get().is_none(), + "node_ref_a should have been reset when the element was reused." + ); + } + + #[test] + async fn vtag_should_not_touch_newly_bound_refs() { + let mut trun = TestRunner::new(); + let test_ref = NodeRef::default(); + + trun.render(html! { + <> +
+ + }) + .await; + trun.render(html! { + <> +
+
+ + }) + .await; + // The point of this diff is that internally, it will first render the "after" div and + // _then_ detach the "before" div, while both should be bound to the same node ref + assert_eq!( + test_ref + .get() + .unwrap() + .dyn_ref::() + .unwrap() + .outer_html(), + "
" + ); + } + + // test for bug: https://github.com/yewstack/yew/pull/2653 + #[test] + async fn test_index_map_attribute_diff() { + let mut trun = TestRunner::new(); + let test_ref = NodeRef::default(); + + // We want to test appy_diff with Attributes::IndexMap, so we + // need to create the VTag manually + + // Create
+ let mut vtag = VTag::new("div"); + vtag.node_ref = test_ref.clone(); + vtag.add_attribute("disabled", "disabled"); + vtag.add_attribute("tabindex", "0"); + + trun.render(VNode::VTag(Box::new(vtag))).await; + + // Create
(removed first attribute "disabled") + let mut vtag = VTag::new("div"); + vtag.node_ref = test_ref.clone(); + vtag.add_attribute("tabindex", "0"); + + // Sync happens here + // this should remove the the "disabled" attribute + trun.render(VNode::VTag(Box::new(vtag))).await; + + assert_eq!( + test_ref + .get() + .unwrap() + .dyn_ref::() + .unwrap() + .outer_html(), + "
" + ) + } +} diff --git a/packages/yew/src/dom_bundle/btext.rs b/packages/yew/src/dom_bundle/btext.rs index 7e6e0f1a908..7dacc08ea72 100644 --- a/packages/yew/src/dom_bundle/btext.rs +++ b/packages/yew/src/dom_bundle/btext.rs @@ -185,46 +185,33 @@ mod layout_tests { use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; use crate::html; - use crate::tests::layout_tests::{diff_layouts, TestLayout}; + use crate::tests::{TestCase, TestRunner}; wasm_bindgen_test_configure!(run_in_browser); #[test] - fn diff() { - let layout1 = TestLayout { - name: "1", - node: html! { "a" }, - expected: "a", - }; - - let layout2 = TestLayout { - name: "2", - node: html! { "b" }, - expected: "b", - }; - - let layout3 = TestLayout { - name: "3", - node: html! { - <> - {"a"} - {"b"} - - }, - expected: "ab", - }; - - let layout4 = TestLayout { - name: "4", - node: html! { - <> - {"b"} - {"a"} - - }, - expected: "ba", - }; - - diff_layouts(vec![layout1, layout2, layout3, layout4]); + async fn diff() { + let mut trun = TestRunner::new(); + + trun.render(html! { "a" }).await.assert_inner_html("a"); + trun.render(html! { "b" }).await.assert_inner_html("b"); + trun.render(html! { + <> + {"a"} + {"b"} + + }) + .await + .assert_inner_html("ab"); + trun.render(html! { + <> + {"b"} + {"a"} + + }) + .await + .assert_inner_html("ba"); + + trun.run_replayable_tests().await; } } diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index a8cac1c900d..d66e42debfc 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -7,8 +7,9 @@ use web_sys::Element; -use crate::html::{AnyScope, NodeRef}; -use crate::virtual_dom::VNode; +use crate::html::{AnyScope, NodeRef, Scope}; +use crate::virtual_dom::{VChild, VNode}; +use crate::BaseComponent; mod bcomp; mod blist; @@ -55,6 +56,23 @@ impl Bundle { self.0.shift(next_parent, next_sibling); } + /// Apply a VChild and return a Scope to the mounted component + pub fn reconcile_vchild( + &mut self, + root: &BSubtree, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + next_node: VChild, + ) -> Scope { + self.reconcile(root, parent_scope, parent, next_sibling, next_node.into()); + let vcomp = match self.0 { + BNode::Comp(ref bcomp) => bcomp, + _ => unreachable!("just mounted a component"), + }; + vcomp.scope().downcast::() + } + /// Applies a virtual dom layout to current bundle. pub fn reconcile( &mut self, @@ -71,6 +89,11 @@ impl Bundle { pub fn detach(self, root: &BSubtree, parent: &Element, parent_to_detach: bool) { self.0.detach(root, parent, parent_to_detach); } + + #[cfg(all(test, target_arch = "wasm32"))] + pub(self) fn as_node(&self) -> &BNode { + &self.0 + } } #[cfg(feature = "hydration")] diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index a35913153a5..8e3d16191b3 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -153,9 +153,15 @@ impl Scope { /// `Option` and `Vec` nicely. `Option` can be used when dealing with a callback that /// might not need to send an update. /// - /// ```ignore - /// link.batch_callback(|_| vec![Msg::A, Msg::B]); - /// link.batch_callback(|_| Some(Msg::A)); + /// ``` + /// # use yew::prelude::*; + /// # use yew::html::Scope; + /// # enum Msg { A, B }; + /// # fn mock>(link: &Scope) { + /// use web_sys::MouseEvent; + /// link.batch_callback(|_: MouseEvent| vec![Msg::A, Msg::B]); + /// link.batch_callback(|_: MouseEvent| Some(Msg::A)); + /// # } /// ``` pub fn batch_callback(&self, function: F) -> Callback where @@ -467,7 +473,7 @@ mod feat_csr { use crate::scheduler; impl AnyScope { - #[cfg(test)] + #[cfg(any(feature = "test_runner"))] pub(crate) fn test() -> Self { Self { type_id: TypeId::of::<()>(), diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index b9bdd9eb257..5555c9f6259 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -28,6 +28,7 @@ //! enable this if your application uses future-based APIs and it does not compile / lint on //! non-wasm32 targets.) //! - `hydration`: Enables Hydration support. +//! - `test_runner`: Enable the [`tests`] module for testing your components //! //! ## Example //! @@ -296,8 +297,8 @@ mod app_handle; #[cfg(feature = "csr")] mod renderer; -#[cfg(feature = "csr")] -#[cfg(test)] +#[cfg(feature = "test_runner")] +#[cfg_attr(documenting, doc(cfg(feature = "test_runner")))] pub mod tests; /// The module that contains all events available in the framework. diff --git a/packages/yew/src/tests/layout_tests.rs b/packages/yew/src/tests/layout_tests.rs deleted file mode 100644 index 2b0ee690b07..00000000000 --- a/packages/yew/src/tests/layout_tests.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! Snapshot testing of Yew components -//! -//! This tests must be run in browser and thus require the `csr` feature to be enabled -use gloo::console::log; -use yew::NodeRef; - -use crate::dom_bundle::{BSubtree, Bundle}; -use crate::html::AnyScope; -use crate::virtual_dom::VNode; -use crate::{scheduler, Component, Context, Html}; - -struct Comp; -impl Component for Comp { - type Message = (); - type Properties = (); - - fn create(_: &Context) -> Self { - unimplemented!() - } - - fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { - unimplemented!(); - } - - fn changed(&mut self, _ctx: &Context) -> bool { - unimplemented!() - } - - fn view(&self, _ctx: &Context) -> Html { - unimplemented!() - } -} - -#[derive(Debug)] -pub struct TestLayout<'a> { - pub name: &'a str, - pub node: VNode, - pub expected: &'a str, -} - -pub fn diff_layouts(layouts: Vec>) { - let document = gloo_utils::document(); - let scope: AnyScope = AnyScope::test(); - let parent_element = document.create_element("div").unwrap(); - let root = BSubtree::create_root(&parent_element); - - let end_node = document.create_text_node("END"); - parent_element.append_child(&end_node).unwrap(); - - // Tests each layout independently - let next_sibling = NodeRef::new(end_node.into()); - for layout in layouts.iter() { - // Apply the layout - let vnode = layout.node.clone(); - log!("Independently apply layout '{}'", layout.name); - - let mut bundle = Bundle::new(); - bundle.reconcile(&root, &scope, &parent_element, next_sibling.clone(), vnode); - scheduler::start_now(); - assert_eq!( - parent_element.inner_html(), - format!("{}END", layout.expected), - "Independent apply failed for layout '{}'", - layout.name, - ); - - // Diff with no changes - let vnode = layout.node.clone(); - - log!("Independently reapply layout '{}'", layout.name); - - bundle.reconcile(&root, &scope, &parent_element, next_sibling.clone(), vnode); - scheduler::start_now(); - assert_eq!( - parent_element.inner_html(), - format!("{}END", layout.expected), - "Independent reapply failed for layout '{}'", - layout.name, - ); - - // Detach - bundle.detach(&root, &parent_element, false); - scheduler::start_now(); - assert_eq!( - parent_element.inner_html(), - "END", - "Independent detach failed for layout '{}'", - layout.name, - ); - } - - // Sequentially apply each layout - let mut bundle = Bundle::new(); - for layout in layouts.iter() { - let next_vnode = layout.node.clone(); - - log!("Sequentially apply layout '{}'", layout.name); - bundle.reconcile( - &root, - &scope, - &parent_element, - next_sibling.clone(), - next_vnode, - ); - - scheduler::start_now(); - assert_eq!( - parent_element.inner_html(), - format!("{}END", layout.expected), - "Sequential apply failed for layout '{}'", - layout.name, - ); - } - - // Sequentially detach each layout - for layout in layouts.into_iter().rev() { - let next_vnode = layout.node.clone(); - - log!("Sequentially detach layout '{}'", layout.name); - bundle.reconcile( - &root, - &scope, - &parent_element, - next_sibling.clone(), - next_vnode, - ); - - scheduler::start_now(); - assert_eq!( - parent_element.inner_html(), - format!("{}END", layout.expected), - "Sequential detach failed for layout '{}'", - layout.name, - ); - } - - // Detach last layout - bundle.detach(&root, &parent_element, false); - scheduler::start_now(); - assert_eq!( - parent_element.inner_html(), - "END", - "Failed to detach last layout" - ); -} diff --git a/packages/yew/src/tests/mod.rs b/packages/yew/src/tests/mod.rs index 7c8881b072f..04408626d8d 100644 --- a/packages/yew/src/tests/mod.rs +++ b/packages/yew/src/tests/mod.rs @@ -1 +1,4 @@ -pub mod layout_tests; +//! Functionality for testing your components +mod runner; + +pub use runner::{TestCase, TestRunner, TestStep}; diff --git a/packages/yew/src/tests/runner.rs b/packages/yew/src/tests/runner.rs new file mode 100644 index 00000000000..6bc26c24b27 --- /dev/null +++ b/packages/yew/src/tests/runner.rs @@ -0,0 +1,488 @@ +//! Testflow based testing of Yew components. +//! +//! This has to be run in the browser, and have the `csr` feature enabled. + +use std::borrow::Cow; +use std::fmt; +use std::future::Future; +use std::panic::Location; +use std::pin::Pin; +use std::time::Duration; + +use gloo::timers::future::sleep; +use web_sys::Element; + +use crate::dom_bundle::{BSubtree, Bundle}; +use crate::html::{AnyScope, Scope}; +use crate::virtual_dom::VChild; +use crate::{scheduler, BaseComponent, Html, NodeRef}; + +#[derive(Debug)] +struct ReplayableLayout { + name: String, + html: Html, + expected: Cow<'static, str>, +} + +/// The test runner controls a piece of the DOM on which your components are mounted. +/// +/// You can then define sub-steps and test various properties of the result of rendering. +pub struct TestRunner { + // Information needed for running the test + scope: AnyScope, + parent: Element, + root: BSubtree, + end_position: NodeRef, + location: &'static Location<'static>, + // Changing over the course of the test + bundle: Bundle, + // Collect a database of fully-specified layouts we can re-test again later + full_layouts: Vec, + unnamed_test_count: usize, +} + +impl fmt::Debug for TestRunner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TestRunner").finish_non_exhaustive() + } +} + +/// Internal helper for things that can be more-or-less treated as a [TestRunner]. +/// +/// Automatically gets an implementation of [TestCase]. +pub trait TestContext { + // Get the underlying runner + fn as_runner(&self) -> &TestRunner; + // Get the underlying runner, mutably + fn as_runner_mut(&mut self) -> &mut TestRunner; + // Get the name of the current context + fn name(&self) -> &str; +} + +impl TestContext for TestRunner { + fn as_runner(&self) -> &TestRunner { + self + } + + fn as_runner_mut(&mut self) -> &mut TestRunner { + self + } + + fn name(&self) -> &str { + "" + } +} + +/// A substep of a flow based test. +/// +/// You can recursively create more substeps, or render and test properties. +/// Borrows from the [`TestRunner`], since you can't run multiples tests on the same piece of DOM +/// concurrently. +pub struct TestStep<'s> { + name: String, + context: &'s mut dyn TestContext, +} + +impl<'s> fmt::Debug for TestStep<'s> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TestStep") + .field("name", &self.name()) + .finish_non_exhaustive() + } +} + +impl<'s> TestContext for TestStep<'s> { + fn as_runner(&self) -> &TestRunner { + self.context.as_runner() + } + + fn as_runner_mut(&mut self) -> &mut TestRunner { + self.context.as_runner_mut() + } + + fn name(&self) -> &str { + &self.name + } +} + +impl TestRunner { + /// Create a new context in which to run tests in the document body. + #[track_caller] + pub fn new() -> Self { + let document = gloo_utils::document(); + let parent = document.create_element("div").unwrap(); + document.body().unwrap().append_child(&parent).unwrap(); + Self::new_in(parent) + } + + /// Create a new context in which to run tests, under the passed parent in the DOM. + #[track_caller] + pub fn new_in(parent: Element) -> Self { + let scope: AnyScope = AnyScope::test(); + let root = BSubtree::create_root(&parent); + + let bundle = Bundle::new(); + + Self { + scope, + parent, + root, + end_position: NodeRef::default(), + location: Location::caller(), + bundle, + full_layouts: vec![], + unnamed_test_count: 0, + } + } + + fn reconcile(&mut self, html: Html) { + self.bundle.reconcile( + &self.root, + &self.scope, + &self.parent, + self.end_position.clone(), + html, + ); + } + + async fn apply(&mut self, html: Html) { + self.reconcile(html.clone()); // Apply the layout twice to catch bad re-apply + scheduler::start_now(); + self.reconcile(html); + scheduler::start_now(); + } + + fn push_replayable_test(&mut self, replayable: ReplayableLayout) { + self.full_layouts.push(replayable); + } + + /// Re-apply "simple" test cases that have been passed in the various stages of the runner. + /// This is a simple way to cross-test the interaction between deterministic layouts by trying + /// different orders and how they get reconciled. + #[track_caller] + pub async fn run_replayable_tests(&mut self) { + let layouts = std::mem::take(&mut self.full_layouts); + + for test in layouts.iter() { + self.apply(test.html.clone()).await; + assert_eq!( + self.parent.inner_html(), + test.expected, + "Sequential apply failed for layout '{}'", + test.name, + ); + } + + for test in layouts.into_iter().rev() { + self.apply(test.html.clone()).await; + assert_eq!( + self.parent.inner_html(), + test.expected, + "Sequential detach failed for layout '{}'", + test.name, + ); + } + + self.reconcile(Html::default()); + scheduler::start_now(); + assert_eq!( + self.parent.inner_html(), + "", + "Sequential detach failed for last layout", + ); + } +} + +impl Default for TestRunner { + fn default() -> Self { + Self::new() + } +} + +/// Access to the dom state after rendering a specific html. +/// +/// Test properties and inspect the dom while you have one of these in sope. +/// Borrows the context, so that you don't accidentally override the dom state with another test. +pub struct TestableState<'s> { + context: &'s mut dyn TestContext, + name: String, + generating_html: Option, + full_layout: Option<&'static str>, +} + +impl<'s> fmt::Debug for TestableState<'s> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TestableState") + .field("name", &self.name) + .finish_non_exhaustive() + } +} + +impl<'s> TestableState<'s> { + /// Retrieve the parent element under which the test has been mounted. + /// + /// Allows directly interacting with dom API, but marks this as a non-"simple" test case. + pub fn parent(&mut self) -> Element { + self.generating_html = None; + self.context.as_runner().parent.clone() + } + + #[cfg(all(test, target_arch = "wasm32"))] + pub(crate) fn bundle(&mut self) -> &Bundle { + self.generating_html = None; + &self.context.as_runner().bundle + } + + /// Test against an exactly given inner html that is supposedly render. + /// + /// Marks this test case as a "simple" test that can be replayed. + #[track_caller] + pub fn assert_inner_html(&mut self, expected: &'static str) { + self.full_layout = Some(expected); + let runner = self.context.as_runner(); + let inner_html = runner.parent.inner_html(); + assert_eq!( + inner_html, expected, + "Independent attach failed for layout '{}'", + self.name, + ); + } +} + +#[cfg(all(feature = "ssr", feature = "hydration"))] +mod feat_ssr_hydrate { + use super::*; + use crate::dom_bundle::Fragment; + use crate::{function_component, html, Properties}; + + #[derive(PartialEq, Properties)] + pub struct HydrateWrapperProps { + pub inner: Html, + } + #[function_component] + pub fn HydrateWrapper(HydrateWrapperProps { inner }: &HydrateWrapperProps) -> Html { + inner.clone() + } + + /// Access to the dom state, just before hydration occurs. + pub struct HydratableState<'s> { + pub(super) context: &'s mut dyn TestContext, + pub(super) name: String, + pub(super) html: Html, + } + + impl<'s> fmt::Debug for HydratableState<'s> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("HydratableState").finish_non_exhaustive() + } + } + + impl<'s> HydratableState<'s> { + /// Hydrate the application now, and wait for it to render + pub async fn hydrate(self) -> TestableState<'s> { + let runner = self.context.as_runner_mut(); + // Hydrate the component into place + let mut fragment = Fragment::collect_children(&runner.parent); + let (_, bundle) = Bundle::hydrate( + &runner.root, + &runner.scope, + &runner.parent, + &mut fragment, + html! { + + }, + ); + runner.bundle = bundle; + // Flush lifecycles, then return + scheduler::start_now(); + TestableState { + context: self.context, + name: self.name, + generating_html: None, + full_layout: None, + } + } + } +} +#[cfg(all(feature = "ssr", feature = "hydration"))] +pub use feat_ssr_hydrate::HydratableState; +#[cfg(all(feature = "ssr", feature = "hydration"))] +use feat_ssr_hydrate::*; +/// Common methods to [TestRunner] and [TestStep] +pub trait TestCase { + /// Enter a sub-step of the test case + fn step(&mut self, name: &'static str) -> TestStep<'_>; + /// Wait for dom events and lifecycle events to be committed to the dom + fn process_events(&mut self) -> Pin>>>; + /// Pretend that `html` was rendered by the server and prepare the dom to hydrate it + #[cfg(all(feature = "ssr", feature = "hydration"))] + fn prepare_hydrate( + &mut self, + html: Html, + ) -> Pin>>>; + /// Await some html to render + fn render(&mut self, html: Html) -> Pin>>>; + /// Await some component to render, returning a testable state and the scope handle to it. + fn render_app( + &mut self, + html: VChild, + ) -> Pin, Scope)>>>; + /// Alias for dropping the [TestCase]. + fn finish(self); +} + +impl TestCase for TC { + fn step(&mut self, name: &'static str) -> TestStep<'_> { + let name = if self.name().is_empty() { + name.to_owned() + } else { + format!("{}.{}", self.name(), name) + }; + TestStep { + name, + context: self, + } + } + + fn process_events(&mut self) -> Pin>>> { + async fn process_events_impl(self_: &mut dyn TestContext) -> TestableState<'_> { + sleep(Duration::ZERO).await; // Wait for the next tick + scheduler::start_now(); + let name = self_.name().to_owned(); + TestableState { + context: self_, + name, + generating_html: None, + full_layout: None, + } + } + Box::pin(process_events_impl(self)) + } + + #[cfg(all(feature = "ssr", feature = "hydration"))] + fn prepare_hydrate( + &mut self, + html: Html, + ) -> Pin>>> { + use yew::ServerRenderer; + + async fn prepare_hydrate_impl( + self_: &mut dyn TestContext, + inner: Html, + ) -> HydratableState<'_> { + // First, render the component with ssr rendering + let rendered = ServerRenderer::::with_props(HydrateWrapperProps { + inner: inner.clone(), + }) + .render() + .await; + // Clear the parent, and replace it with ssr rendering result + let runner = self_.as_runner_mut(); + runner.reconcile(Html::default()); + runner.parent.set_inner_html(&rendered); + HydratableState { + name: self_.name().to_owned(), + context: self_, + html: inner, + } + } + Box::pin(prepare_hydrate_impl(self, html)) + } + + fn render(&mut self, html: Html) -> Pin>>> { + async fn render_impl(self_: &mut dyn TestContext, html: Html) -> TestableState<'_> { + let name = self_.name().to_owned(); + let runner = self_.as_runner_mut(); + runner.apply(html.clone()).await; + TestableState { + context: self_, + name, + generating_html: Some(html), + full_layout: None, + } + } + Box::pin(render_impl(self, html)) + } + + fn render_app( + &mut self, + html: VChild, + ) -> Pin, Scope)>>> { + async fn render_app_impl( + self_: &mut dyn TestContext, + html: VChild, + ) -> (TestableState<'_>, Scope) { + let name = self_.name().to_owned(); + let runner = self_.as_runner_mut(); + let scope = runner.bundle.reconcile_vchild( + &runner.root, + &runner.scope, + &runner.parent, + runner.end_position.clone(), + html, + ); + scheduler::start_now(); + let state = TestableState { + context: self_, + name, + generating_html: None, + full_layout: None, + }; + (state, scope) + } + Box::pin(render_app_impl(self, html)) + } + + fn finish(self) {} +} + +impl<'s> Drop for TestStep<'s> { + // Not #[track_caller], since that always points to ptr::mod.rs drop glue + fn drop(&mut self) { + let runner = self.context.as_runner_mut(); + runner.reconcile(Html::default()); + scheduler::start_now(); + assert_eq!( + runner.parent.inner_html(), + "", + "Independent detach failed for layout '{}'", + self.name(), + ); + gloo::console::log!(format!("Layout '{}' ... ok", self.name())); + } +} + +impl<'s> Drop for TestableState<'s> { + fn drop(&mut self) { + if self.name.is_empty() { + let runner = self.context.as_runner_mut(); + runner.unnamed_test_count += 1; + } + if let Some(html) = self.generating_html.take() { + if let Some(full_layout) = self.full_layout.take() { + let saved_layout = ReplayableLayout { + name: self.context.name().to_owned(), + html, + expected: full_layout.into(), + }; + self.context + .as_runner_mut() + .push_replayable_test(saved_layout); + } + } + } +} + +impl Drop for TestRunner { + fn drop(&mut self) { + if self.unnamed_test_count > 0 { + gloo::console::log!(format!( + "[{}:{}:{}] {} unnamed layouts ... ok", + self.location.file(), + self.location.line(), + self.location.column(), + self.unnamed_test_count + )); + } + } +} diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index c330b12ec8e..6169c075eda 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -161,8 +161,8 @@ pub struct VChild { /// The component properties pub props: Rc, /// Reference to the mounted node - node_ref: NodeRef, - key: Option, + pub(crate) node_ref: NodeRef, + pub(crate) key: Option, } impl Clone for VChild { diff --git a/packages/yew/tests/common/mod.rs b/packages/yew/tests/common/mod.rs index 8da829ab7d3..5593b7dca0c 100644 --- a/packages/yew/tests/common/mod.rs +++ b/packages/yew/tests/common/mod.rs @@ -1,5 +1,16 @@ #![allow(dead_code)] +use std::cell::{Cell, RefCell}; +use std::future::Future; +use std::pin::Pin; +use std::rc::Rc; +use std::task::{Context, Poll}; +use slab::Slab; +use yew::html::ImplicitClone; +use yew::suspense::{Suspension, SuspensionHandle, SuspensionResult}; +use yew::{hook, use_reducer, use_state, Reducible}; + +#[track_caller] pub fn obtain_result() -> String { gloo_utils::document() .get_element_by_id("result") @@ -7,9 +18,134 @@ pub fn obtain_result() -> String { .inner_html() } +#[track_caller] pub fn obtain_result_by_id(id: &str) -> String { gloo_utils::document() .get_element_by_id(id) .expect("No result found. Most likely, the application crashed and burned") .inner_html() } + +#[derive(Clone)] +pub struct TriggerBus(Rc<(Cell, RefCell>>)>); + +impl TriggerBus { + pub fn new() -> Self { + Self(Rc::new((Cell::new(false), RefCell::new(Slab::new())))) + } + + pub fn activate(&self) { + let this = &self.0; + if !this.0.get() { + this.0.set(true); // no race problem + let mut reg = this.1.borrow_mut(); + for (_, t) in reg.iter_mut() { + t(true); + } + } + } + + pub fn deactivate(&self) { + let this = &self.0; + if this.0.get() { + this.0.set(false); // no race problem + let mut reg = this.1.borrow_mut(); + for (_, t) in reg.iter_mut() { + t(false); + } + } + } + + fn read(&self) -> bool { + self.0 .0.get() + } + + fn subscribe(&self, trigger: Box) -> usize { + self.0 .1.borrow_mut().insert(trigger) + } + + fn unsubscribe(&self, id: usize) { + let _ = self.0 .1.borrow_mut().remove(id); + } +} + +impl Future for TriggerBus { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self.read() { + Poll::Ready(()) + } else { + let mut waker = Some(cx.waker().clone()); + self.subscribe(Box::new(move |b: bool| { + if !b { + return; + } + if let Some(w) = waker.take() { + w.wake() + } + })); + // soft todo: bother to unsubscribe + Poll::Pending + } + } +} + +impl PartialEq for TriggerBus { + fn eq(&self, _: &Self) -> bool { + true // Pretend, don't cause re-renders + } +} + +impl ImplicitClone for TriggerBus {} + +#[hook] +pub fn use_trigger(bus: &TriggerBus) -> SuspensionResult> { + struct BusTracker { + suspension: RefCell>, + } + impl Reducible for BusTracker { + type Action = bool; + + fn reduce(self: Rc, action: bool) -> Rc { + match action { + true => { + if let Some(susp) = self.suspension.borrow_mut().take() { + susp.1.resume() + } + } + false => *self.suspension.borrow_mut() = Some(Suspension::new()), + } + self + } + } + struct BusSubscriber { + id: usize, + bus: TriggerBus, + } + impl Drop for BusSubscriber { + fn drop(&mut self) { + self.bus.unsubscribe(self.id) + } + } + let sleep_state = use_reducer(|| BusTracker { + suspension: RefCell::new((!bus.read()).then(Suspension::new)), + }); + let _ = { + let sleep_state = sleep_state.dispatcher(); + let bus = bus.clone(); + use_state(move || { + let id = bus.subscribe(Box::new(move |b| sleep_state.dispatch(b))); + BusSubscriber { id, bus } + }) + }; + + let state = (*sleep_state).suspension.borrow(); + match &*state { + Some(ref state) if !state.0.resumed() => Err(state.0.clone()), + _ => { + let bus = bus.clone(); + Ok(Box::new(move || bus.deactivate())) + } + } +} diff --git a/packages/yew/tests/hydration.rs b/packages/yew/tests/hydration.rs index b0d249015fe..76afc82f6de 100644 --- a/packages/yew/tests/hydration.rs +++ b/packages/yew/tests/hydration.rs @@ -1,20 +1,18 @@ #![cfg(feature = "hydration")] #![cfg(target_arch = "wasm32")] -use std::rc::Rc; -use std::time::Duration; +use std::clone::Clone; +use std::cmp::PartialEq; mod common; -use common::{obtain_result, obtain_result_by_id}; -use gloo::timers::future::sleep; -use wasm_bindgen::JsCast; -use wasm_bindgen_futures::spawn_local; +use common::{use_trigger, TriggerBus}; use wasm_bindgen_test::*; use web_sys::{HtmlElement, HtmlTextAreaElement}; use yew::prelude::*; -use yew::suspense::{use_future, Suspension, SuspensionResult}; -use yew::{Renderer, ServerRenderer}; +use yew::suspense::SuspensionResult; +use yew::tests::{TestCase, TestRunner}; +use yew::{use_state, Callback, NodeRef}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -23,8 +21,13 @@ wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] async fn hydration_works() { + #[derive(PartialEq, Properties)] + struct CounterProps { + button_ref: NodeRef, + } + #[function_component] - fn Comp() -> Html { + fn Counter(CounterProps { button_ref }: &CounterProps) -> Html { let ctr = use_state_eq(|| 0); let onclick = { @@ -38,104 +41,37 @@ async fn hydration_works() { html! {
{"Counter: "}{*ctr} - -
- } - } - - #[function_component] - fn App() -> Html { - html! { -
- +
} } - let s = ServerRenderer::::new().render().await; - - gloo::utils::document() - .query_selector("#output") - .unwrap() - .unwrap() - .set_inner_html(&s); - - sleep(Duration::ZERO).await; - - Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) - .hydrate(); - - sleep(Duration::ZERO).await; - - let result = obtain_result_by_id("output"); - + let button_ref = NodeRef::default(); + let mut trun = TestRunner::new(); // no placeholders, hydration is successful. - assert_eq!( - result, - r#"
Counter: 0
"# - ); - - gloo_utils::document() - .query_selector(".increase") - .unwrap() - .unwrap() - .dyn_into::() - .unwrap() - .click(); - - sleep(Duration::ZERO).await; - - let result = obtain_result_by_id("output"); - - assert_eq!( - result, - r#"
Counter: 1
"# - ); + trun.prepare_hydrate(html! { }) + .await + .hydrate() + .await + .assert_inner_html(r#"
Counter: 0
"#); + button_ref.cast::().unwrap().click(); + trun.process_events() + .await + .assert_inner_html(r#"
Counter: 1
"#); } #[wasm_bindgen_test] async fn hydration_with_suspense() { - #[derive(PartialEq)] - pub struct SleepState { - s: Suspension, - } - - impl SleepState { - fn new() -> Self { - let (s, handle) = Suspension::new(); - - spawn_local(async move { - sleep(Duration::from_millis(50)).await; - - handle.resume(); - }); - - Self { s } - } - } - - impl Reducible for SleepState { - type Action = (); - - fn reduce(self: Rc, _action: Self::Action) -> Rc { - Self::new().into() - } + #[derive(PartialEq, Properties, Clone)] + struct TriggerProps { + trigger: TriggerBus, + break_ref: NodeRef, + inc_ref: NodeRef, } - #[hook] - pub fn use_sleep() -> SuspensionResult> { - 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()?; + #[function_component] + fn Content(props: &TriggerProps) -> HtmlResult { + let resleep = use_trigger(&props.trigger)?; let value = use_state(|| 0); @@ -147,150 +83,77 @@ async fn hydration_with_suspense() { }) }; - let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())()); + let on_take_a_break = Callback::from(move |_: MouseEvent| resleep()); Ok(html! {
{*value}
- +
- +
}) } #[function_component(App)] - fn app() -> Html { + fn app(props: &TriggerProps) -> Html { let fallback = html! {
{"wait..."}
}; html! { -
- - - -
+ + + } } - - let s = ServerRenderer::::new().render().await; - - gloo::utils::document() - .query_selector("#output") - .unwrap() - .unwrap() - .set_inner_html(&s); - - sleep(Duration::ZERO).await; - - Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) - .hydrate(); - - sleep(Duration::from_millis(10)).await; - - let result = obtain_result(); - - // still hydrating, during hydration, the server rendered result is shown. - assert_eq!( - result.as_str(), - r#"
0
"# + let mut trun = TestRunner::new(); + let trigger = TriggerBus::new(); + let break_ref = NodeRef::default(); + let inc_ref = NodeRef::default(); + trigger.activate(); + let ssr = trun + .prepare_hydrate(html! { + + }) + .await; + trigger.deactivate(); + ssr.hydrate() + .await.assert_inner_html( + r#"
0
"# ); - sleep(Duration::from_millis(50)).await; - - let result = obtain_result(); - - // hydrated. - assert_eq!( - result.as_str(), - r#"
0
"# + trigger.activate(); + trun.process_events().await.assert_inner_html( + r#"
0
"# ); - gloo_utils::document() - .query_selector(".increase") - .unwrap() - .unwrap() - .dyn_into::() - .unwrap() - .click(); - - sleep(Duration::from_millis(50)).await; - - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
1
"# + inc_ref.cast::().unwrap().click(); + trun.process_events().await.assert_inner_html( + r#"
1
"# ); - gloo_utils::document() - .query_selector(".take-a-break") - .unwrap() - .unwrap() - .dyn_into::() - .unwrap() - .click(); - - sleep(Duration::from_millis(10)).await; - let result = obtain_result(); - assert_eq!(result.as_str(), "
wait...
"); + break_ref.cast::().unwrap().click(); + trun.process_events() + .await + .assert_inner_html("
wait...
"); - sleep(Duration::from_millis(50)).await; - - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
1
"# + trigger.activate(); + trun.process_events().await.assert_inner_html( + r#"
1
"# ); } #[wasm_bindgen_test] async fn hydration_with_suspense_not_suspended_at_start() { - #[derive(PartialEq)] - pub struct SleepState { - s: Option, - } - - impl SleepState { - fn new() -> Self { - Self { s: None } - } - } - - impl Reducible for SleepState { - type Action = (); - - fn reduce(self: Rc, _action: Self::Action) -> Rc { - let (s, handle) = Suspension::new(); - - spawn_local(async move { - sleep(Duration::from_millis(50)).await; - - handle.resume(); - }); - - Self { s: Some(s) }.into() - } + #[derive(PartialEq, Properties, Clone)] + struct ContentProps { + trigger: TriggerBus, + break_ref: NodeRef, } - #[hook] - pub fn use_sleep() -> SuspensionResult> { - 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()?; + #[function_component] + fn Content(ContentProps { trigger, break_ref }: &ContentProps) -> HtmlResult { + let resleep = use_trigger(trigger)?; let value = use_state(|| "I am writing a long story...".to_string()); @@ -304,612 +167,412 @@ async fn hydration_with_suspense_not_suspended_at_start() { }) }; - let on_take_a_break = Callback::from(move |_| (resleep.clone())()); + let on_take_a_break = Callback::from(move |_| resleep()); Ok(html! {
- +
}) } - #[function_component(App)] - fn app() -> Html { + #[function_component] + fn App(props: &ContentProps) -> Html { let fallback = html! {
{"wait..."}
}; html! { -
- - - -
+ + + } } - let s = ServerRenderer::::new().render().await; - - gloo::utils::document() - .query_selector("#output") - .unwrap() - .unwrap() - .set_inner_html(&s); - - sleep(Duration::ZERO).await; - - Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) - .hydrate(); - - sleep(Duration::from_millis(10)).await; - - let result = obtain_result(); - - assert_eq!( - result.as_str(), - r#"
"# + let mut trun = TestRunner::new(); + let trigger = TriggerBus::new(); + let break_ref = NodeRef::default(); + trigger.activate(); + trun.prepare_hydrate(html! { + + }) + .await + .hydrate() + .await.assert_inner_html( + r#"
"# ); - gloo_utils::document() - .query_selector(".take-a-break") - .unwrap() - .unwrap() - .dyn_into::() - .unwrap() - .click(); - - sleep(Duration::from_millis(10)).await; - - let result = obtain_result(); - assert_eq!(result.as_str(), "
wait...
"); - sleep(Duration::from_millis(50)).await; + break_ref.cast::().unwrap().click(); + trun.process_events() + .await + .assert_inner_html("
wait...
"); - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
"# + trigger.activate(); + trun.process_events().await.assert_inner_html( + r#"
"# ); } #[wasm_bindgen_test] async fn hydration_nested_suspense_works() { - #[derive(PartialEq)] - pub struct SleepState { - s: Suspension, + #[derive(PartialEq, Properties, Clone)] + struct NestedProps { + outer_trigger: TriggerBus, + inner_trigger: TriggerBus, + outer_break_ref: NodeRef, + inner_break_ref: NodeRef, } - impl SleepState { - fn new() -> Self { - let (s, handle) = Suspension::new(); - - spawn_local(async move { - sleep(Duration::from_millis(50)).await; - - handle.resume(); - }); - - Self { s } - } - } - - impl Reducible for SleepState { - type Action = (); - - fn reduce(self: Rc, _action: Self::Action) -> Rc { - Self::new().into() - } - } - - #[hook] - pub fn use_sleep() -> SuspensionResult> { - 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())()); + #[function_component] + fn InnerContent(props: &NestedProps) -> HtmlResult { + let resleep = use_trigger(&props.inner_trigger)?; + let on_take_a_break = Callback::from(move |_: MouseEvent| resleep()); Ok(html! {
- +
}) } - #[function_component(Content)] - fn content() -> HtmlResult { - let resleep = use_sleep()?; - + #[function_component] + fn Content(props: &NestedProps) -> HtmlResult { + let resleep = use_trigger(&props.outer_trigger)?; + let on_take_a_break = Callback::from(move |_: MouseEvent| resleep()); let fallback = html! {
{"wait...(inner)"}
}; - let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())()); - Ok(html! {
- +
- +
}) } - #[function_component(App)] - fn app() -> Html { + #[function_component] + fn App(props: &NestedProps) -> Html { let fallback = html! {
{"wait...(outer)"}
}; html! { -
- - - -
+ + + } } - - let s = ServerRenderer::::new().render().await; - - gloo::utils::document() - .query_selector("#output") - .unwrap() - .unwrap() - .set_inner_html(&s); - - sleep(Duration::ZERO).await; - - Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) - .hydrate(); - - // outer suspense is hydrating... - sleep(Duration::from_millis(10)).await; - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
"# + let mut trun = TestRunner::new(); + let outer_trigger = TriggerBus::new(); + let inner_trigger = TriggerBus::new(); + let outer_break_ref = NodeRef::default(); + let inner_break_ref = NodeRef::default(); + + outer_trigger.activate(); + inner_trigger.activate(); // for ssr + let ssr = trun.prepare_hydrate(html! { + + }).await; + outer_trigger.deactivate(); + inner_trigger.deactivate(); + ssr.hydrate() + .await.assert_inner_html( + r#"
"# ); - sleep(Duration::from_millis(50)).await; - + outer_trigger.activate(); // inner suspense is hydrating... - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
"# + trun.process_events().await.assert_inner_html( + r#"
"# ); - sleep(Duration::from_millis(50)).await; - + inner_trigger.activate(); // hydrated. - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
"# + trun.process_events().await.assert_inner_html( + r#"
"# ); - gloo_utils::document() - .query_selector(".take-a-break") - .unwrap() - .unwrap() - .dyn_into::() - .unwrap() - .click(); - - sleep(Duration::from_millis(10)).await; - - let result = obtain_result(); - assert_eq!(result.as_str(), "
wait...(outer)
"); - - sleep(Duration::from_millis(50)).await; + outer_break_ref.cast::().unwrap().click(); + trun.process_events() + .await + .assert_inner_html("
wait...(outer)
"); - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
"# + outer_trigger.activate(); + trun.process_events().await.assert_inner_html( + r#"
"# ); - gloo_utils::document() - .query_selector(".take-a-break2") - .unwrap() - .unwrap() - .dyn_into::() - .unwrap() - .click(); - - sleep(Duration::from_millis(10)).await; - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
wait...(inner)
"# + inner_break_ref.cast::().unwrap().click(); + trun.process_events().await.assert_inner_html( + r#"
wait...(inner)
"# ); - sleep(Duration::from_millis(50)).await; - - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
"# + inner_trigger.activate(); + trun.process_events().await.assert_inner_html( + r#"
"# ); } #[wasm_bindgen_test] async fn hydration_node_ref_works() { - #[function_component(App)] - pub fn app() -> Html { - let size = use_state(|| 4); - - let callback = { - let size = size.clone(); - Callback::from(move |_| { - size.set(10); - }) - }; - - html! { -
- -
- } - } - - #[derive(Properties, PartialEq)] - struct ListProps { - size: u32, - } - - #[function_component(Test1)] - fn test1() -> Html { + #[function_component] + fn Test1() -> Html { html! { {"test"} } } - #[function_component(Test2)] - fn test2() -> Html { + #[function_component] + fn Test2() -> Html { html! { } } - - #[function_component(List)] - fn list(props: &ListProps) -> Html { - let elems = 0..props.size; - - html! { - <> - { for elems.map(|_| + #[derive(Properties, PartialEq)] + struct ListProps { + size: u32, + } + #[function_component] + fn List(props: &ListProps) -> Html { + (0..props.size) + .map(|_| { html! { } - )} - - } + }) + .collect() } + #[derive(Properties, PartialEq)] + struct AppProps { + click_ref: NodeRef, + } + #[function_component] + fn App(AppProps { click_ref }: &AppProps) -> Html { + let size = use_state(|| 4); - let s = ServerRenderer::::new().render().await; - - gloo::utils::document() - .query_selector("#output") - .unwrap() - .unwrap() - .set_inner_html(&s); - - sleep(Duration::ZERO).await; - - Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) - .hydrate(); + let callback = { + let size = size.clone(); + Callback::from(move |_| size.set(10)) + }; - sleep(Duration::ZERO).await; + html! { +
+ +
+ } + } - let result = obtain_result_by_id("output"); - assert_eq!( - result.as_str(), - r#"
testtesttesttest
"# + let mut trun = TestRunner::new(); + let click_ref = NodeRef::default(); + trun.prepare_hydrate(html! { + + }) + .await + .hydrate() + .await + .assert_inner_html( + r#"
testtesttesttest
"#, ); - gloo_utils::document() - .query_selector("span") - .unwrap() - .unwrap() - .dyn_into::() - .unwrap() - .click(); - - sleep(Duration::ZERO).await; - - let result = obtain_result_by_id("output"); - assert_eq!( - result.as_str(), + click_ref.cast::().unwrap().click(); + trun.process_events().await.assert_inner_html( r#"
testtesttesttesttesttesttesttesttesttest
"# ); } -#[wasm_bindgen_test] -async fn hydration_list_order_works() { - #[function_component(App)] - pub fn app() -> Html { - let elems = 0..10; +#[derive(Properties, PartialEq, Clone)] +struct NumberProps { + number: u32, + trigger: TriggerBus, +} - html! { - <> - { for elems.map(|number| - html! { - - } - )} - - } +#[function_component] +fn Number(props: &NumberProps) -> Html { + html! { +
{props.number}
} +} - #[derive(Properties, PartialEq)] - struct NumberProps { - number: u32, - } +#[hook] +pub fn use_suspend(trigger: &TriggerBus) -> SuspensionResult<()> { + let _ = use_trigger(trigger)?; + Ok(()) +} - #[function_component(Number)] - fn number(props: &NumberProps) -> Html { - html! { -
{props.number.to_string()}
- } +#[function_component] +fn SuspendedNumber(props: &NumberProps) -> HtmlResult { + use_suspend(&props.trigger)?; + Ok(html! { +
{props.number}
+ }) +} + +#[function_component] +fn ToSuspendOrNot(props: &NumberProps) -> Html { + let number = props.number; + html! { + + if number % 3 == 0 { + + } else { + + } + } - #[function_component(SuspendedNumber)] - fn suspended_number(props: &NumberProps) -> HtmlResult { - use_suspend()?; - Ok(html! { -
{props.number.to_string()}
- }) +} + +#[wasm_bindgen_test] +async fn hydration_list_order_works() { + #[derive(Properties, PartialEq)] + struct AppProps { + trigger: TriggerBus, } - #[function_component(ToSuspendOrNot)] - fn suspend_or_not(props: &NumberProps) -> Html { - let number = props.number; - html! { - - if number % 3 == 0 { - - } else { - + #[function_component] + fn App(AppProps { trigger }: &AppProps) -> Html { + (0..10) + .map(|number| { + html! { + } - - } - } - - #[hook] - pub fn use_suspend() -> SuspensionResult<()> { - use_future(|| async {})?; - Ok(()) + }) + .collect() } - let s = ServerRenderer::::new().render().await; - - gloo::utils::document() - .query_selector("#output") - .unwrap() - .unwrap() - .set_inner_html(&s); - - sleep(Duration::ZERO).await; - - Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) - .hydrate(); - - // Wait until all suspended components becomes revealed. - sleep(Duration::ZERO).await; - sleep(Duration::ZERO).await; - sleep(Duration::ZERO).await; - sleep(Duration::ZERO).await; - - let result = obtain_result_by_id("output"); - assert_eq!( - result.as_str(), - // Until all components become revealed, there will be component markers. - // As long as there's no component markers all components have become unsuspended. + let mut trun = TestRunner::new(); + let trigger = TriggerBus::new(); + trigger.activate(); // for ssr + let ssr = trun + .prepare_hydrate(html! { + + }) + .await; + trigger.deactivate(); + ssr.hydrate().await; + // Reveal all suspended components. + trigger.activate(); + // Until all components become revealed, there will be component markers. + // As long as there's no component markers all components have become unsuspended. + trun.process_events().await.assert_inner_html( r#"
0
1
2
3
4
5
6
7
8
9
"# ); } #[wasm_bindgen_test] async fn hydration_suspense_no_flickering() { - #[function_component(App)] - pub fn app() -> Html { - let fallback = html! {

{"Loading..."}

}; - html! { - - - - } + #[derive(PartialEq, Properties, Clone)] + struct SuspendProps { + inner_bus: TriggerBus, + outer_bus: TriggerBus, } - #[derive(Properties, PartialEq, Clone)] struct NumberProps { number: u32, + busses: SuspendProps, } - - #[function_component(SuspendedNumber)] - fn suspended_number(props: &NumberProps) -> HtmlResult { - use_suspend()?; - - Ok(html! { - - }) - } - #[function_component(Number)] - fn number(props: &NumberProps) -> Html { + #[function_component] + fn Number(props: &NumberProps) -> Html { html! {
{props.number.to_string()}
} } + #[function_component] + fn SuspendedNumber(props: &NumberProps) -> HtmlResult { + use_suspend(&props.busses.inner_bus)?; - #[function_component(Suspended)] - fn suspended() -> HtmlResult { - use_suspend()?; + Ok(html! { + + }) + } + #[function_component] + fn Suspended(props: &SuspendProps) -> HtmlResult { + use_suspend(&props.outer_bus)?; Ok(html! { { for (0..10).map(|number| html! { - + } )} }) } - - #[hook] - pub fn use_suspend() -> SuspensionResult<()> { - use_future(|| async { - gloo::timers::future::sleep(std::time::Duration::from_millis(50)).await; - })?; - Ok(()) + #[function_component] + fn App(props: &SuspendProps) -> Html { + let fallback = html! {

{"Loading..."}

}; + html! { + + + + } } - let s = ServerRenderer::::new().render().await; + let mut trun = TestRunner::new(); + let inner_bus = TriggerBus::new(); + let outer_bus = TriggerBus::new(); - gloo::utils::document() - .query_selector("#output") - .unwrap() - .unwrap() - .set_inner_html(&s); - - sleep(Duration::ZERO).await; - - Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) - .hydrate(); - - // Wait until all suspended components becomes revealed. - sleep(Duration::ZERO).await; - - let result = obtain_result_by_id("output"); - assert_eq!( - result.as_str(), - // outer still suspended. - r#"
0
1
2
3
4
5
6
7
8
9
"# - ); - sleep(Duration::from_millis(26)).await; - - let result = obtain_result_by_id("output"); - assert_eq!( - result.as_str(), - r#"
0
1
2
3
4
5
6
7
8
9
"# - ); - sleep(Duration::from_millis(26)).await; + inner_bus.activate(); + outer_bus.activate(); // for ssr + let ssr = trun + .prepare_hydrate(html! { + + }) + .await; + inner_bus.deactivate(); + outer_bus.deactivate(); - let result = obtain_result_by_id("output"); - assert_eq!( - result.as_str(), + ssr.hydrate().await.assert_inner_html( r#"
0
1
2
3
4
5
6
7
8
9
"# ); - sleep(Duration::from_millis(26)).await; - let result = obtain_result_by_id("output"); - assert_eq!( - result.as_str(), - // outer revealed, inner still suspended, outer remains. + outer_bus.activate(); + trun.process_events().await.assert_inner_html( r#"
0
1
2
3
4
5
6
7
8
9
"# ); - sleep(Duration::from_millis(26)).await; - - let result = obtain_result_by_id("output"); - assert_eq!( - result.as_str(), - // inner revealed. + inner_bus.activate(); + // inner revealed. + trun.process_events().await.assert_inner_html( r#"
0
1
2
3
4
5
6
7
8
9
"# ); } #[wasm_bindgen_test] async fn hydration_order_issue_nested_suspense() { - #[function_component(App)] - pub fn app() -> Html { - let elems = (0..10).map(|number: u32| { - html! { - - } - }); - - html! { - - { for elems } - - } - } - #[derive(Properties, PartialEq)] - struct NumberProps { - number: u32, + struct AppProps { + trigger: TriggerBus, } - - #[function_component(Number)] - fn number(props: &NumberProps) -> Html { + #[function_component] + fn App(AppProps { trigger }: &AppProps) -> Html { html! { -
{props.number.to_string()}
+ + { for (0..10).map(|number: u32| { + html! { + + } + })} + } } - #[function_component(SuspendedNumber)] - fn suspended_number(props: &NumberProps) -> HtmlResult { - use_suspend()?; - Ok(html! { -
{props.number.to_string()}
- }) - } - - #[function_component(ToSuspendOrNot)] - fn suspend_or_not(props: &NumberProps) -> HtmlResult { - let number = props.number; - Ok(html! { - if number % 3 == 0 { - - - - } else { - - } + let mut trun = TestRunner::new(); + let trigger = TriggerBus::new(); + trigger.activate(); // for ssr + let ssr = trun + .prepare_hydrate(html! { + }) - } - - #[hook] - pub fn use_suspend() -> SuspensionResult<()> { - use_future(|| async {})?; - - Ok(()) - } - - let s = ServerRenderer::::new().render().await; - - gloo::utils::document() - .query_selector("#output") - .unwrap() - .unwrap() - .set_inner_html(&s); - - sleep(Duration::ZERO).await; - - Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) - .hydrate(); - - // Wait until all suspended components becomes revealed. - sleep(Duration::ZERO).await; - sleep(Duration::ZERO).await; - sleep(Duration::ZERO).await; - sleep(Duration::ZERO).await; - - let result = obtain_result_by_id("output"); - assert_eq!( - result.as_str(), - // Until all components become revealed, there will be component markers. - // As long as there's no component markers all components have become unsuspended. + .await; + trigger.deactivate(); + ssr.hydrate().await; + trigger.activate(); + // Until all components become revealed, there will be component markers. + // As long as there's no component markers all components have become unsuspended. + trun.process_events().await.assert_inner_html( r#"
0
1
2
3
4
5
6
7
8
9
"# ); } diff --git a/packages/yew/tests/mod.rs b/packages/yew/tests/mod.rs index f61f01d24f8..46dbaa47c00 100644 --- a/packages/yew/tests/mod.rs +++ b/packages/yew/tests/mod.rs @@ -2,12 +2,9 @@ mod common; -use std::time::Duration; - -use common::obtain_result; -use gloo::timers::future::sleep; use wasm_bindgen_test::*; use yew::prelude::*; +use yew::tests::{TestCase, TestRunner}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -20,23 +17,15 @@ async fn props_are_passed() { #[function_component] fn PropsComponent(props: &PropsPassedFunctionProps) -> Html { - assert_eq!(&props.value, "props"); html! { -
- {"done"} -
+ {&props.value} } } - yew::Renderer::::with_root_and_props( - gloo_utils::document().get_element_by_id("output").unwrap(), - PropsPassedFunctionProps { - value: "props".to_string(), - }, - ) - .render(); - - sleep(Duration::ZERO).await; - let result = obtain_result(); - assert_eq!(result.as_str(), "done"); + TestRunner::new() + .render(html! { + + }) + .await + .assert_inner_html("props123"); } diff --git a/packages/yew/tests/suspense.rs b/packages/yew/tests/suspense.rs index eb045a7c37b..9c4ca0158c0 100644 --- a/packages/yew/tests/suspense.rs +++ b/packages/yew/tests/suspense.rs @@ -2,7 +2,7 @@ mod common; -use common::obtain_result; +use common::{use_trigger, TriggerBus}; use wasm_bindgen_test::*; use yew::prelude::*; @@ -11,451 +11,234 @@ 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::{use_future, use_future_with_deps, Suspension, SuspensionResult}; +use web_sys::HtmlElement; +use yew::suspense::{use_future, use_future_with_deps}; +use yew::tests::{TestCase, TestRunner}; #[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, _action: Self::Action) -> Rc { - Self::new().into() - } - } - - #[hook] - pub fn use_sleep() -> SuspensionResult> { - 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()?; - + #[derive(PartialEq, Properties, Clone)] + struct AppProps { + inc_ref: NodeRef, + break_ref: NodeRef, + resleep_bus: TriggerBus, + } + #[function_component] + fn Content(props: &AppProps) -> HtmlResult { + let resleep = use_trigger(&props.resleep_bus)?; let value = use_state(|| 0); let on_increment = { let value = value.clone(); - Callback::from(move |_: MouseEvent| { - value.set(*value + 1); - }) + Callback::from(move |_: MouseEvent| value.set(*value + 1)) }; - let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())()); + let on_take_a_break = Callback::from(move |_: MouseEvent| resleep()); Ok(html! {
{*value}
- +
- +
}) } - #[function_component(App)] - fn app() -> Html { + #[function_component] + fn App(props: &AppProps) -> Html { let fallback = html! {
{"wait..."}
}; html! { -
- - - -
+ + + } } - yew::Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) - .render(); - - TimeoutFuture::new(10).await; - let result = obtain_result(); - assert_eq!(result.as_str(), "
wait...
"); + let mut trun = TestRunner::new(); + let break_ref = NodeRef::default(); + let inc_ref = NodeRef::default(); + let resleep_bus = TriggerBus::new(); + trun.render(html! { + + }) + .await + .assert_inner_html("
wait...
"); - TimeoutFuture::new(50).await; - - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
0
"# + resleep_bus.activate(); + trun.process_events().await.assert_inner_html( + r#"
0
"# ); - TimeoutFuture::new(10).await; - - gloo_utils::document() - .query_selector(".increase") - .unwrap() - .unwrap() - .dyn_into::() - .unwrap() - .click(); - - TimeoutFuture::new(0).await; - - gloo_utils::document() - .query_selector(".increase") - .unwrap() - .unwrap() - .dyn_into::() - .unwrap() - .click(); + inc_ref.cast::().unwrap().click(); + trun.process_events().await; + inc_ref.cast::().unwrap().click(); - TimeoutFuture::new(1).await; - - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
2
"# + trun.process_events().await.assert_inner_html( + r#"
2
"# ); - gloo_utils::document() - .query_selector(".take-a-break") - .unwrap() - .unwrap() - .dyn_into::() - .unwrap() - .click(); - - TimeoutFuture::new(10).await; - let result = obtain_result(); - assert_eq!(result.as_str(), "
wait...
"); - - TimeoutFuture::new(50).await; + break_ref.cast::().unwrap().click(); + trun.process_events() + .await + .assert_inner_html("
wait...
"); - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
2
"# + resleep_bus.activate(); + trun.process_events().await.assert_inner_html( + r#"
2
"# ); } #[wasm_bindgen_test] async fn suspense_not_suspended_at_start() { - #[derive(PartialEq)] - pub struct SleepState { - s: Option, - } - - impl SleepState { - fn new() -> Self { - Self { s: None } - } - } - - impl Reducible for SleepState { - type Action = (); - - fn reduce(self: Rc, _action: Self::Action) -> Rc { - 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> { - 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()?; - + #[derive(PartialEq, Properties, Clone)] + struct AppProps { + break_ref: NodeRef, + resleep_bus: TriggerBus, + } + #[function_component] + fn Content(props: &AppProps) -> HtmlResult { + let resleep = use_trigger(&props.resleep_bus)?; 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())()); + let on_take_a_break = Callback::from(move |_| resleep()); Ok(html! {
- +
- +
}) } - - #[function_component(App)] - fn app() -> Html { + #[function_component] + fn App(props: &AppProps) -> Html { let fallback = html! {
{"wait..."}
}; html! { -
- - - -
+ + + } } - yew::Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) - .render(); + let mut trun = TestRunner::new(); + let break_ref = NodeRef::default(); + let resleep_bus = TriggerBus::new(); + resleep_bus.activate(); + trun.render(html! { + + }) + .await + .assert_inner_html( + r#"
"# + ); - TimeoutFuture::new(10).await; + break_ref.cast::().unwrap().click(); + trun.process_events() + .await + .assert_inner_html("
wait...
"); - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
"# - ); - gloo_utils::document() - .query_selector(".take-a-break") - .unwrap() - .unwrap() - .dyn_into::() - .unwrap() - .click(); - - TimeoutFuture::new(10).await; - let result = obtain_result(); - assert_eq!(result.as_str(), "
wait...
"); - - TimeoutFuture::new(50).await; - - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
"# + resleep_bus.activate(); + trun.process_events().await.assert_inner_html( + r#"
"# ); } #[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, _action: Self::Action) -> Rc { - Self::new().into() - } - } - - #[hook] - pub fn use_sleep() -> SuspensionResult> { - 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())()); + #[derive(PartialEq, Properties, Clone)] + struct AppProps { + outer_trigger: TriggerBus, + inner_trigger: TriggerBus, + outer_break_ref: NodeRef, + inner_break_ref: NodeRef, + } + #[function_component] + fn InnerContent(props: &AppProps) -> HtmlResult { + let resleep = use_trigger(&props.inner_trigger)?; + let on_take_a_break = Callback::from(move |_: MouseEvent| resleep()); Ok(html! {
- +
}) } - - #[function_component(Content)] - fn content() -> HtmlResult { - let resleep = use_sleep()?; + #[function_component] + fn Content(props: &AppProps) -> HtmlResult { + let resleep = use_trigger(&props.outer_trigger)?; + let on_take_a_break = Callback::from(move |_: MouseEvent| resleep()); let fallback = html! {
{"wait...(inner)"}
}; - let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())()); - Ok(html! {
- +
- +
}) } - - #[function_component(App)] - fn app() -> Html { + #[function_component] + fn App(props: &AppProps) -> Html { let fallback = html! {
{"wait...(outer)"}
}; html! { -
- - - -
+ + + } } - yew::Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) - .render(); - - TimeoutFuture::new(10).await; - let result = obtain_result(); - assert_eq!(result.as_str(), "
wait...(outer)
"); + let mut trun = TestRunner::new(); + let outer_trigger = TriggerBus::new(); + let inner_trigger = TriggerBus::new(); + let outer_break_ref = NodeRef::default(); + let inner_break_ref = NodeRef::default(); - TimeoutFuture::new(50).await; + trun.render(html! { + + }).await.assert_inner_html("
wait...(outer)
"); - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
wait...(inner)
"# + outer_trigger.activate(); + trun.process_events().await.assert_inner_html( + r#"
wait...(inner)
"# ); - TimeoutFuture::new(50).await; - - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
"# + inner_trigger.activate(); + trun.process_events().await.assert_inner_html( + r#"
"# ); - gloo_utils::document() - .query_selector(".take-a-break2") - .unwrap() - .unwrap() - .dyn_into::() - .unwrap() - .click(); - - TimeoutFuture::new(10).await; - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
wait...(inner)
"# + inner_break_ref.cast::().unwrap().click(); + trun.process_events().await.assert_inner_html( + r#"
wait...(inner)
"# ); - TimeoutFuture::new(50).await; - - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
"# + inner_trigger.activate(); + trun.process_events().await.assert_inner_html( + r#"
"# ); } #[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, _action: Self::Action) -> Rc { - Self::new().into() - } - } - - #[hook] - pub fn use_sleep() -> SuspensionResult> { - 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>, + trigger: TriggerBus, + break_ref: NodeRef, + inc_ref: NodeRef, } impl PartialEq for Props { @@ -464,142 +247,100 @@ async fn effects_not_run_when_suspended() { } } - #[function_component(Content)] - fn content(props: &Props) -> HtmlResult { + #[function_component] + fn Content(props: &Props) -> HtmlResult { { let counter = props.counter.clone(); use_effect(move || { - let mut counter = counter.borrow_mut(); - - *counter += 1; - + *counter.borrow_mut() += 1; || {} }); } - let resleep = use_sleep()?; - + let resleep = use_trigger(&props.trigger)?; let value = use_state(|| 0); let on_increment = { let value = value.clone(); - Callback::from(move |_: MouseEvent| { - value.set(*value + 1); - }) + Callback::from(move |_: MouseEvent| value.set(*value + 1)) }; - let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())()); + let on_take_a_break = Callback::from(move |_: MouseEvent| resleep()); Ok(html! {
{*value}
- +
- +
}) } - - #[function_component(App)] - fn app(props: &Props) -> Html { + #[function_component] + fn App(props: &Props) -> Html { let fallback = html! {
{"wait..."}
}; html! { -
- - - -
+ + + } } + let mut trun = TestRunner::new(); let counter = Rc::new(RefCell::new(0_u64)); - - let props = Props { - counter: counter.clone(), - }; - - yew::Renderer::::with_root_and_props( - gloo_utils::document().get_element_by_id("output").unwrap(), - props, - ) - .render(); - - TimeoutFuture::new(10).await; - let result = obtain_result(); - assert_eq!(result.as_str(), "
wait...
"); + let break_ref = NodeRef::default(); + let inc_ref = NodeRef::default(); + let trigger = TriggerBus::new(); + + trun.render(html! { + + }) + .await + .assert_inner_html("
wait...
"); assert_eq!(*counter.borrow(), 0); // effects not called. - TimeoutFuture::new(50).await; - - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
0
"# + trigger.activate(); + trun.process_events().await.assert_inner_html( + r#"
0
"# ); assert_eq!(*counter.borrow(), 1); // effects ran 1 time. - TimeoutFuture::new(10).await; - - gloo_utils::document() - .query_selector(".increase") - .unwrap() - .unwrap() - .dyn_into::() - .unwrap() - .click(); - - TimeoutFuture::new(0).await; - - gloo_utils::document() - .query_selector(".increase") - .unwrap() - .unwrap() - .dyn_into::() - .unwrap() - .click(); - - TimeoutFuture::new(0).await; - - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
2
"# + inc_ref.cast::().unwrap().click(); + trun.process_events().await; + inc_ref.cast::().unwrap().click(); + trun.process_events().await.assert_inner_html( + r#"
2
"# ); assert_eq!(*counter.borrow(), 3); // effects ran 3 times. - gloo_utils::document() - .query_selector(".take-a-break") - .unwrap() - .unwrap() - .dyn_into::() - .unwrap() - .click(); - - TimeoutFuture::new(10).await; - let result = obtain_result(); - assert_eq!(result.as_str(), "
wait...
"); - assert_eq!(*counter.borrow(), 3); // effects ran 3 times. - - TimeoutFuture::new(50).await; + break_ref.cast::().unwrap().click(); + trun.process_events() + .await + .assert_inner_html("
wait...
"); + assert_eq!(*counter.borrow(), 3); // effects still ran 3 times. - let result = obtain_result(); - assert_eq!( - result.as_str(), - r#"
2
"# + trigger.activate(); + trun.process_events().await.assert_inner_html( + r#"
2
"# ); assert_eq!(*counter.borrow(), 4); // effects ran 4 times. } #[wasm_bindgen_test] async fn use_suspending_future_works() { - #[function_component(Content)] - fn content() -> HtmlResult { + #[derive(PartialEq, Properties, Clone)] + struct AppProps { + trigger: TriggerBus, + } + #[function_component] + fn Content(AppProps { trigger }: &AppProps) -> HtmlResult { + let trigger = trigger.clone(); let _sleep_handle = use_future(|| async move { - TimeoutFuture::new(50).await; + trigger.await; })?; Ok(html! { @@ -608,79 +349,81 @@ async fn use_suspending_future_works() {
}) } - - #[function_component(App)] - fn app() -> Html { - let fallback = html! {
{"wait..."}
}; + #[function_component] + fn App(props: &AppProps) -> Html { + let fallback = html! {
{"wait..."}
}; html! { -
- - - -
+ + + } } - yew::Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) - .render(); + let mut trun = TestRunner::new(); + let trigger = TriggerBus::new(); - TimeoutFuture::new(10).await; - let result = obtain_result(); - assert_eq!(result.as_str(), "
wait...
"); + trun.render(html! { + + }) + .await + .assert_inner_html("
wait...
"); - TimeoutFuture::new(50).await; - - let result = obtain_result(); - assert_eq!(result.as_str(), r#"
Content
"#); + trigger.activate(); + trun.process_events() + .await + .assert_inner_html(r#"
Content
"#); } #[wasm_bindgen_test] async fn use_suspending_future_with_deps_works() { #[derive(PartialEq, Properties)] struct ContentProps { - delay_millis: u32, + dep_value: u32, + trigger: TriggerBus, } - #[function_component(Content)] - fn content(ContentProps { delay_millis }: &ContentProps) -> HtmlResult { + fn content(props: &ContentProps) -> HtmlResult { + let trigger = props.trigger.clone(); let delayed_result = use_future_with_deps( - |delay_millis| async move { - TimeoutFuture::new(*delay_millis).await; - 42 + |dep_value| async move { + trigger.await; + dep_value }, - *delay_millis, + props.dep_value, )?; Ok(html! {
- {*delayed_result} + {&*delayed_result}
}) } - - #[function_component(App)] - fn app() -> Html { + #[derive(PartialEq, Properties)] + struct AppProps { + trigger: TriggerBus, + } + #[function_component] + fn App(AppProps { trigger }: &AppProps) -> Html { let fallback = html! {
{"wait..."}
}; html! { -
- - - -
+ + + } } - yew::Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) - .render(); - - TimeoutFuture::new(10).await; - let result = obtain_result(); - assert_eq!(result.as_str(), "
wait...
"); - - TimeoutFuture::new(50).await; + let mut trun = TestRunner::new(); + let trigger = TriggerBus::new(); + trun.render(html! { + + }) + .await + .assert_inner_html("
wait...
"); - let result = obtain_result(); - assert_eq!(result.as_str(), r#"
42
"#); + trigger.activate(); + trun.process_events() + .await + .assert_inner_html(r#"
42
"#); } diff --git a/packages/yew/tests/use_callback.rs b/packages/yew/tests/use_callback.rs index b7f8a45c1f8..cb2ae0c9116 100644 --- a/packages/yew/tests/use_callback.rs +++ b/packages/yew/tests/use_callback.rs @@ -4,12 +4,9 @@ use std::sync::atomic::{AtomicBool, Ordering}; mod common; -use std::time::Duration; - -use common::obtain_result; -use gloo::timers::future::sleep; use wasm_bindgen_test::*; use yew::prelude::*; +use yew::tests::{TestCase, TestRunner}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -31,16 +28,12 @@ async fn use_callback_works() { } html! { -
- {"The test output is: "} -
{&greeting}
- {"\n"} -
+ <>{"Callback output: "}{&greeting} } } - #[function_component(UseCallbackComponent)] - fn use_callback_comp() -> Html { + #[function_component] + fn UseCallbackComponent() -> Html { let state = use_state(|| 0); let callback = use_callback(move |name, _| format!("Hello, {}!", name), ()); @@ -54,19 +47,12 @@ async fn use_callback_works() { }); html! { -
- -
+ } } - yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), - ) - .render(); - - sleep(Duration::ZERO).await; - - let result = obtain_result(); - assert_eq!(result.as_str(), "Hello, Yew!"); + let mut trun = TestRunner::new(); + trun.render(html! { }) + .await + .assert_inner_html("Callback output: Hello, Yew!"); } diff --git a/packages/yew/tests/use_context.rs b/packages/yew/tests/use_context.rs index 19f58f494e1..d1008e041e5 100644 --- a/packages/yew/tests/use_context.rs +++ b/packages/yew/tests/use_context.rs @@ -9,6 +9,7 @@ use common::obtain_result_by_id; use gloo::timers::future::sleep; use wasm_bindgen_test::*; use yew::prelude::*; +use yew::tests::{TestCase, TestRunner}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -64,15 +65,12 @@ async fn use_context_scoping_works() { } } - yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), - ) - .render(); - - sleep(Duration::ZERO).await; - - let result: String = obtain_result_by_id("result"); - assert_eq!("correct", result); + let mut trun = TestRunner::new(); + trun.render(html! { + + }) + .await + .assert_inner_html(r#"
ignored
ignored
correct
ignored
"#); } #[wasm_bindgen_test] @@ -147,12 +145,11 @@ async fn use_context_works_with_multiple_types() { } } - yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), - ) - .render(); - - sleep(Duration::ZERO).await; + TestRunner::new() + .render(html! { + + }) + .await; } #[wasm_bindgen_test] diff --git a/packages/yew/tests/use_effect.rs b/packages/yew/tests/use_effect.rs index c7969544be5..b0c22e62447 100644 --- a/packages/yew/tests/use_effect.rs +++ b/packages/yew/tests/use_effect.rs @@ -4,12 +4,10 @@ mod common; use std::ops::{Deref, DerefMut}; use std::rc::Rc; -use std::time::Duration; -use common::obtain_result; -use gloo::timers::future::sleep; use wasm_bindgen_test::*; use yew::prelude::*; +use yew::tests::{TestCase, TestRunner}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -35,8 +33,8 @@ async fn use_effect_destroys_on_component_drop() { } } - #[function_component(UseEffectComponent)] - fn use_effect_comp(props: &FunctionProps) -> Html { + #[function_component] + fn UseEffectComponent(props: &FunctionProps) -> Html { let effect_called = props.effect_called.clone(); let destroy_called = props.destroy_called.clone(); use_effect_with_deps( @@ -50,8 +48,8 @@ async fn use_effect_destroys_on_component_drop() { html! {} } - #[function_component(UseEffectWrapperComponent)] - fn use_effect_wrapper_comp(props: &WrapperProps) -> Html { + #[function_component] + fn UseEffectWrapperComponent(props: &WrapperProps) -> Html { let show = use_state(|| true); if *show { let effect_called: Rc = { Rc::new(move || show.set(false)) }; @@ -67,23 +65,21 @@ async fn use_effect_destroys_on_component_drop() { let destroy_counter = Rc::new(std::cell::RefCell::new(0)); let destroy_counter_c = destroy_counter.clone(); - yew::Renderer::::with_root_and_props( - gloo_utils::document().get_element_by_id("output").unwrap(), - WrapperProps { - destroy_called: Rc::new(move || *destroy_counter_c.borrow_mut().deref_mut() += 1), - }, - ) - .render(); - - sleep(Duration::ZERO).await; + TestRunner::new() + .render(html! { + + } /> + }) + .await; assert_eq!(1, *destroy_counter.borrow().deref()); } #[wasm_bindgen_test] async fn use_effect_works_many_times() { - #[function_component(UseEffectComponent)] - fn use_effect_comp() -> Html { + #[function_component] + fn UseEffectComponent() -> Html { let counter = use_state(|| 0); let counter_clone = counter.clone(); @@ -98,22 +94,19 @@ async fn use_effect_works_many_times() { ); html! { -
- { "The test result is" } -
{ *counter }
- { "\n" } -
+ <> + { "The test result is: " } + { *counter } + } } - yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), - ) - .render(); - - sleep(Duration::ZERO).await; - let result = obtain_result(); - assert_eq!(result.as_str(), "4"); + TestRunner::new() + .render(html! { + + }) + .await + .assert_inner_html("The test result is: 4"); } #[wasm_bindgen_test] @@ -132,23 +125,19 @@ async fn use_effect_works_once() { ); html! { -
- { "The test result is" } -
{ *counter }
- { "\n" } -
+ <> + { "The test result is: " } + { *counter } + } } - yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), - ) - .render(); - sleep(Duration::ZERO).await; - - let result = obtain_result(); - - assert_eq!(result.as_str(), "1"); + TestRunner::new() + .render(html! { + + }) + .await + .assert_inner_html("The test result is: 1"); } #[wasm_bindgen_test] @@ -180,21 +169,19 @@ async fn use_effect_refires_on_dependency_change() { arg, ); html! { -
- {"The test result is"} -
{*number_ref.borrow_mut().deref_mut()}{*number_ref2.borrow_mut().deref_mut()}
- {"\n"} -
+ <> + { "The test result is: " } + { *number_ref.borrow_mut().deref_mut() } + { ";" } + { *number_ref2.borrow_mut().deref_mut() } + } } - yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), - ) - .render(); - - sleep(Duration::ZERO).await; - let result: String = obtain_result(); - - assert_eq!(result.as_str(), "11"); + TestRunner::new() + .render(html! { + + }) + .await + .assert_inner_html("The test result is: 1;1"); } diff --git a/packages/yew/tests/use_memo.rs b/packages/yew/tests/use_memo.rs index c62b71852c6..419176f2929 100644 --- a/packages/yew/tests/use_memo.rs +++ b/packages/yew/tests/use_memo.rs @@ -4,19 +4,16 @@ use std::sync::atomic::{AtomicBool, Ordering}; mod common; -use std::time::Duration; - -use common::obtain_result; -use gloo::timers::future::sleep; use wasm_bindgen_test::*; use yew::prelude::*; +use yew::tests::{TestCase, TestRunner}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] async fn use_memo_works() { - #[function_component(UseMemoComponent)] - fn use_memo_comp() -> Html { + #[function_component] + fn UseMemoComponent() -> Html { let state = use_state(|| 0); let memoed_val = use_memo( @@ -41,21 +38,17 @@ async fn use_memo_works() { }); html! { -
- {"The test output is: "} -
{*memoed_val}
- {"\n"} -
+ <> + { "The test result is: " } + { *memoed_val } + } } - yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), - ) - .render(); - - sleep(Duration::ZERO).await; - - let result = obtain_result(); - assert_eq!(result.as_str(), "true"); + TestRunner::new() + .render(html! { + + }) + .await + .assert_inner_html("The test result is: true"); } diff --git a/packages/yew/tests/use_reducer.rs b/packages/yew/tests/use_reducer.rs index dddde7c8541..1c068d21d06 100644 --- a/packages/yew/tests/use_reducer.rs +++ b/packages/yew/tests/use_reducer.rs @@ -10,6 +10,7 @@ use wasm_bindgen::JsCast; use wasm_bindgen_test::*; use web_sys::HtmlElement; use yew::prelude::*; +use yew::tests::{TestCase, TestRunner}; mod common; @@ -35,8 +36,8 @@ impl Reducible for CounterState { #[wasm_bindgen_test] async fn use_reducer_works() { - #[function_component(UseReducerComponent)] - fn use_reducer_comp() -> Html { + #[function_component] + fn UseReducerComponent() -> Html { let counter = use_reducer(|| CounterState { counter: 10 }); let counter_clone = counter.clone(); @@ -48,22 +49,19 @@ async fn use_reducer_works() { (), ); html! { -
- {"The test result is"} -
{counter.counter}
- {"\n"} -
+ <> + { "The test result is: " } + { counter.counter } + } } - yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), - ) - .render(); - sleep(Duration::ZERO).await; - let result = obtain_result(); - - assert_eq!(result.as_str(), "11"); + TestRunner::new() + .render(html! { + + }) + .await + .assert_inner_html("The test result is: 11"); } #[derive(Debug, Clone, PartialEq)] @@ -83,8 +81,8 @@ impl Reducible for ContentState { #[wasm_bindgen_test] async fn use_reducer_eq_works() { - #[function_component(UseReducerComponent)] - fn use_reducer_comp() -> Html { + #[function_component] + fn UseReducerComponent() -> Html { let content = use_reducer_eq(|| ContentState { content: HashSet::default(), }); diff --git a/packages/yew/tests/use_ref.rs b/packages/yew/tests/use_ref.rs index d0e34e4dc8a..e0d35721035 100644 --- a/packages/yew/tests/use_ref.rs +++ b/packages/yew/tests/use_ref.rs @@ -3,19 +3,17 @@ mod common; use std::ops::DerefMut; -use std::time::Duration; -use common::obtain_result; -use gloo::timers::future::sleep; use wasm_bindgen_test::*; use yew::prelude::*; +use yew::tests::{TestCase, TestRunner}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] async fn use_ref_works() { - #[function_component(UseRefComponent)] - fn use_ref_comp() -> Html { + #[function_component] + fn UseRefComponent() -> Html { let ref_example = use_mut_ref(|| 0); *ref_example.borrow_mut().deref_mut() += 1; let counter = use_state(|| 0); @@ -23,20 +21,17 @@ async fn use_ref_works() { counter.set(*counter + 1) } html! { -
- {"The test output is: "} -
{*ref_example.borrow_mut().deref_mut() > 4}
- {"\n"} -
+ <> + {"The test result is: "} + { *ref_example.borrow_mut().deref_mut() > 4 } + } } - yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), - ) - .render(); - sleep(Duration::ZERO).await; - - let result = obtain_result(); - assert_eq!(result.as_str(), "true"); + TestRunner::new() + .render(html! { + + }) + .await + .assert_inner_html("The test result is: true"); } diff --git a/packages/yew/tests/use_state.rs b/packages/yew/tests/use_state.rs index 68800dae0f9..137c625ed52 100644 --- a/packages/yew/tests/use_state.rs +++ b/packages/yew/tests/use_state.rs @@ -2,45 +2,40 @@ mod common; -use std::time::Duration; - -use common::obtain_result; -use gloo::timers::future::sleep; use wasm_bindgen_test::*; use yew::prelude::*; +use yew::tests::{TestCase, TestRunner}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] async fn use_state_works() { - #[function_component(UseComponent)] - fn use_state_comp() -> Html { + #[function_component] + fn UseComponent() -> Html { let counter = use_state(|| 0); if *counter < 5 { counter.set(*counter + 1) } html! { -
- {"Test Output: "} -
{*counter}
- {"\n"} -
+ <> + { "Test Output: " } + { *counter } + } } - yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), - ) - .render(); - sleep(Duration::ZERO).await; - let result = obtain_result(); - assert_eq!(result.as_str(), "5"); + TestRunner::new() + .render(html! { + + }) + .await + .assert_inner_html("Test Output: 5"); } #[wasm_bindgen_test] async fn multiple_use_state_setters() { - #[function_component(UseComponent)] - fn use_state_comp() -> Html { + #[function_component] + fn UseComponent() -> Html { let counter = use_state(|| 0); let counter_clone = counter.clone(); use_effect_with_deps( @@ -62,22 +57,19 @@ async fn multiple_use_state_setters() { }; another_scope(); html! { -
+ <> { "Test Output: " } - // expected output -
{ *counter }
- { "\n" } -
+ { *counter } + } } - yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), - ) - .render(); - sleep(Duration::ZERO).await; - let result = obtain_result(); - assert_eq!(result.as_str(), "11"); + TestRunner::new() + .render(html! { + + }) + .await + .assert_inner_html("Test Output: 11"); } #[wasm_bindgen_test] @@ -85,27 +77,25 @@ async fn use_state_eq_works() { use std::sync::atomic::{AtomicUsize, Ordering}; static RENDER_COUNT: AtomicUsize = AtomicUsize::new(0); - #[function_component(UseComponent)] - fn use_state_comp() -> Html { + #[function_component] + fn UseComponent() -> Html { RENDER_COUNT.fetch_add(1, Ordering::Relaxed); let counter = use_state_eq(|| 0); counter.set(1); html! { -
- {"Test Output: "} -
{*counter}
- {"\n"} -
+ <> + { "Test Output: " } + { *counter } + } } - yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), - ) - .render(); - sleep(Duration::ZERO).await; - let result = obtain_result(); - assert_eq!(result.as_str(), "1"); + TestRunner::new() + .render(html! { + + }) + .await + .assert_inner_html("Test Output: 1"); assert_eq!(RENDER_COUNT.load(Ordering::Relaxed), 2); }