Skip to content

Commit 60b729e

Browse files
committed
add e2e Performance API fetch counting and navigation tests
1 parent 4e9e867 commit 60b729e

13 files changed

Lines changed: 239 additions & 46 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/ssr_router/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ ssr-e2e-harness = { path = "../../tools/ssr-e2e-harness" }
4747

4848
[dev-dependencies]
4949
yew = { path = "../../packages/yew", features = ["hydration"] }
50+
yew-link = { path = "../../packages/yew-link", features = ["hydration"] }
5051

5152
[features]
5253
ssr = ["yew/ssr", "yew-link/ssr", "yew-link/axum"]
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
use ssr_router::App;
1+
use ssr_router::{App, AppProps, LINK_ENDPOINT};
22

33
fn main() {
44
#[cfg(target_arch = "wasm32")]
55
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
6-
yew::Renderer::<App>::new().hydrate();
6+
yew::Renderer::<App>::with_props(AppProps {
7+
endpoint: LINK_ENDPOINT.into(),
8+
})
9+
.hydrate();
710
}

examples/ssr_router/src/bin/ssr_router_server.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ use futures::stream::{self, StreamExt};
1717
use hyper::body::Incoming;
1818
use hyper_util::rt::TokioIo;
1919
use hyper_util::server;
20-
use ssr_router::{LinkedAuthor, LinkedPost, LinkedPostMeta, ServerApp, ServerAppProps};
20+
use ssr_router::{
21+
LinkedAuthor, LinkedPost, LinkedPostMeta, ServerApp, ServerAppProps, LINK_ENDPOINT,
22+
};
2123
use tokio::net::TcpListener;
2224
use tower::Service;
2325
use tower_http::cors::CorsLayer;
@@ -134,7 +136,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
134136

135137
let app = Router::new()
136138
.route(
137-
"/api/link",
139+
LINK_ENDPOINT,
138140
post(linked_state_handler).with_state(arc_resolver),
139141
)
140142
.fallback_service(

examples/ssr_router/src/lib.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use yew::prelude::*;
88
use yew_link::{linked_state, use_linked_state, LinkProvider, ResolverProp};
99
use yew_router::prelude::*;
1010

11+
pub const LINK_ENDPOINT: &str = "/api/link";
12+
1113
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1214
pub struct LinkedPost(pub content::Post);
1315

@@ -402,11 +404,16 @@ fn switch(routes: function_router::Route) -> Html {
402404
}
403405
}
404406

407+
#[derive(Properties, PartialEq)]
408+
pub struct AppProps {
409+
pub endpoint: AttrValue,
410+
}
411+
405412
#[component]
406-
pub fn App() -> Html {
413+
pub fn App(props: &AppProps) -> Html {
407414
html! {
408415
<BrowserRouter>
409-
<LinkProvider endpoint="/api/link">
416+
<LinkProvider endpoint={props.endpoint.clone()}>
410417
<function_router::components::nav::Nav />
411418
<main>
412419
<Switch<function_router::Route> render={switch} />
@@ -442,7 +449,7 @@ pub fn ServerApp(props: &ServerAppProps) -> Html {
442449

443450
html! {
444451
<Router history={history}>
445-
<LinkProvider endpoint="/api/link" resolver={props.resolver.clone()}>
452+
<LinkProvider endpoint={LINK_ENDPOINT} resolver={props.resolver.clone()}>
446453
<function_router::components::nav::Nav />
447454
<main>
448455
<Switch<function_router::Route> render={switch} />

examples/ssr_router/tests/e2e.rs

Lines changed: 126 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
11
#![cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
22

3-
use std::time::Duration;
4-
5-
use function_router::App;
63
use gloo::utils::document;
7-
use ssr_e2e_harness::{output_element, setup_ssr_page, wait_for};
4+
use ssr_e2e_harness::{
5+
clear_resource_timings, fetch_ssr_html, navigate, output_element, resource_request_count,
6+
setup_ssr_page, wait_for,
7+
};
8+
use ssr_router::{App, AppProps, LINK_ENDPOINT};
89
use wasm_bindgen_test::*;
9-
use yew::platform::time::sleep;
10+
use yew::Renderer;
1011

1112
wasm_bindgen_test_configure!(run_in_browser);
1213

1314
const SERVER_BASE: &str = "http://127.0.0.1:8080";
1415

16+
fn endpoint() -> String {
17+
format!("{SERVER_BASE}{LINK_ENDPOINT}")
18+
}
19+
20+
fn make_renderer() -> Renderer<App> {
21+
Renderer::<App>::with_root_and_props(
22+
output_element(),
23+
AppProps {
24+
endpoint: endpoint().into(),
25+
},
26+
)
27+
}
28+
1529
fn get_title_text() -> Option<String> {
1630
document()
1731
.query_selector("h1.title")
@@ -20,29 +34,69 @@ fn get_title_text() -> Option<String> {
2034
.map(|el| el.text_content().unwrap_or_default())
2135
}
2236

37+
fn post_body_text() -> String {
38+
output_element()
39+
.query_selector(".section.container")
40+
.ok()
41+
.flatten()
42+
.map(|el| el.text_content().unwrap_or_default())
43+
.unwrap_or_default()
44+
}
45+
46+
fn extract_text_from_html(html: &str, selector: &str) -> Option<String> {
47+
let container = document().create_element("div").unwrap();
48+
container.set_inner_html(html);
49+
container
50+
.query_selector(selector)
51+
.ok()
52+
.flatten()
53+
.and_then(|el| el.text_content())
54+
}
55+
2356
#[wasm_bindgen_test]
24-
async fn hydrate_post_page() {
25-
setup_ssr_page(SERVER_BASE, "/posts/0").await;
26-
yew::Renderer::<App>::with_root(output_element()).hydrate();
57+
async fn ssr_hydration_and_client_navigation() {
58+
// -- Part 1: Direct SSR visit to /posts/0 triggers no fetch to /api/link --
59+
60+
let ssr_html = fetch_ssr_html(SERVER_BASE, "/posts/0").await;
61+
let ssr_title = extract_text_from_html(&ssr_html, "h1.title")
62+
.expect("SSR HTML for /posts/0 should contain h1.title");
63+
let ssr_body = extract_text_from_html(&ssr_html, ".section.container").unwrap_or_default();
64+
65+
clear_resource_timings();
66+
67+
output_element().set_inner_html(&ssr_html);
68+
ssr_e2e_harness::push_route("/posts/0");
69+
let app = make_renderer().hydrate();
2770

2871
wait_for(
2972
|| {
3073
let html = output_element().inner_html();
3174
html.contains("<h1 class=\"title\">") && !html.contains("Loading post...")
3275
},
3376
5000,
34-
"post page content",
77+
"post page content after SSR hydration",
3578
)
3679
.await;
3780

38-
let title = get_title_text().expect("h1.title should be present on the post page");
39-
assert!(!title.is_empty(), "post title should not be empty");
40-
}
81+
let link_fetches = resource_request_count(LINK_ENDPOINT);
82+
let title = get_title_text();
4183

42-
#[wasm_bindgen_test]
43-
async fn hydrate_posts_list() {
44-
setup_ssr_page(SERVER_BASE, "/posts").await;
45-
yew::Renderer::<App>::with_root(output_element()).hydrate();
84+
assert_eq!(
85+
link_fetches, 0,
86+
"direct SSR visit to /posts/0 should not trigger any fetch to {LINK_ENDPOINT}"
87+
);
88+
let title = title.expect("h1.title should be present on the SSR post page");
89+
assert!(!title.is_empty(), "SSR post title should not be empty");
90+
91+
// -- Part 2: Navigate to /posts within the same app, then to /posts/0 --
92+
93+
// Yield to ensure effects (router history listener) are registered.
94+
gloo::timers::future::sleep(std::time::Duration::from_millis(500)).await;
95+
96+
clear_resource_timings();
97+
98+
// Navigate to /posts first, then to /posts/0 to trigger a client-side fetch.
99+
navigate("/posts");
46100

47101
wait_for(
48102
|| {
@@ -51,22 +105,69 @@ async fn hydrate_posts_list() {
51105
.ok()
52106
.flatten()
53107
.is_some()
108+
&& get_title_text().as_deref() == Some("Posts")
54109
},
55-
10000,
56-
"post links to appear on /posts",
110+
15000,
111+
"posts list after client-side navigation to /posts",
57112
)
58113
.await;
114+
115+
clear_resource_timings();
116+
117+
navigate("/posts/0");
118+
119+
wait_for(
120+
|| {
121+
document()
122+
.query_selector("h2.subtitle")
123+
.ok()
124+
.flatten()
125+
.map(|el| el.text_content().unwrap_or_default())
126+
.is_some_and(|text| text.starts_with("by "))
127+
},
128+
15000,
129+
"post page content after client-side navigation to /posts/0",
130+
)
131+
.await;
132+
133+
// -- Part 3: Verify fetch happened and content matches SSR --
134+
135+
let nav_link_fetches = resource_request_count(LINK_ENDPOINT);
136+
let nav_title = get_title_text();
137+
let nav_body = post_body_text();
138+
139+
assert!(
140+
nav_link_fetches >= 1,
141+
"client-side navigation to /posts/0 should trigger at least one fetch to {LINK_ENDPOINT}, \
142+
got {nav_link_fetches}"
143+
);
144+
145+
let nav_title = nav_title.expect("h1.title should be present after client-side navigation");
146+
assert_eq!(
147+
ssr_title, nav_title,
148+
"post title should match between SSR and client-side navigation"
149+
);
150+
assert_eq!(
151+
ssr_body, nav_body,
152+
"post body should match between SSR and client-side navigation"
153+
);
154+
155+
app.destroy();
156+
output_element().set_inner_html("");
59157
}
60158

61159
#[wasm_bindgen_test]
62160
async fn hydrate_home() {
63161
setup_ssr_page(SERVER_BASE, "/").await;
64-
yew::Renderer::<App>::with_root(output_element()).hydrate();
162+
let app = make_renderer().hydrate();
65163

66-
sleep(Duration::from_secs(2)).await;
67-
let html = output_element().inner_html();
68-
assert!(
69-
html.contains("Welcome"),
70-
"home page should have content after hydration"
71-
);
164+
wait_for(
165+
|| output_element().inner_html().contains("Welcome"),
166+
5000,
167+
"home page content after hydration",
168+
)
169+
.await;
170+
171+
app.destroy();
172+
output_element().set_inner_html("");
72173
}

packages/yew-router/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ features = [
3333
]
3434

3535
[dev-dependencies]
36-
wasm-bindgen-test = "0.3"
36+
wasm-bindgen-test.workspace = true
3737
serde = { workspace = true, features = ["derive"] }
3838
yew = { version = "0.22.1", path = "../yew", features = ["csr"] }
3939

packages/yew/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] }
8585
tokio = { workspace = true, features = ["macros", "rt", "time"] }
8686

8787
[dev-dependencies]
88-
wasm-bindgen-test = "0.3"
88+
wasm-bindgen-test.workspace = true
8989
gloo = { workspace = true, features = ["futures"] }
9090
wasm-bindgen-futures.workspace = true
9191
trybuild = { workspace = true }

packages/yew/src/dom_bundle/btag/listeners.rs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -406,8 +406,8 @@ mod tests {
406406
input
407407
.dispatch_event(
408408
&Event::new_with_event_init_dict("blur", &{
409-
let mut dict = EventInit::new();
410-
dict.bubbles(false);
409+
let dict = EventInit::new();
410+
dict.set_bubbles(false);
411411
dict
412412
})
413413
.unwrap(),
@@ -731,21 +731,23 @@ mod tests {
731731
#[test]
732732
fn oninput() {
733733
test_input_listener(|| {
734-
web_sys::InputEvent::new_with_event_init_dict(
735-
"input",
736-
web_sys::InputEventInit::new().bubbles(true),
737-
)
734+
web_sys::InputEvent::new_with_event_init_dict("input", &{
735+
let init = web_sys::InputEventInit::new();
736+
init.set_bubbles(true);
737+
init
738+
})
738739
.unwrap()
739740
})
740741
}
741742

742743
#[test]
743744
fn onchange() {
744745
test_input_listener(|| {
745-
web_sys::Event::new_with_event_init_dict(
746-
"change",
747-
web_sys::EventInit::new().bubbles(true),
748-
)
746+
web_sys::Event::new_with_event_init_dict("change", &{
747+
let init = web_sys::EventInit::new();
748+
init.set_bubbles(true);
749+
init
750+
})
749751
.unwrap()
750752
})
751753
}

tools/ssr-e2e-harness/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@ edition = "2021"
55

66
[dependencies]
77
gloo = { workspace = true, features = ["futures"] }
8+
gloo-history = "0.2"
89
web-sys = { workspace = true }
910
wasm-bindgen = { workspace = true }
11+
js-sys = { workspace = true }

0 commit comments

Comments
 (0)