Skip to content

Commit 6e0dbf2

Browse files
committed
fixed navigation and performance API based fetch counting
1 parent 6e73bbb commit 6e0dbf2

File tree

7 files changed

+208
-139
lines changed

7 files changed

+208
-139
lines changed

Cargo.lock

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

examples/simple_ssr/tests/e2e.rs

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

33
use simple_ssr::App;
4-
use ssr_e2e_harness::{fetch_ssr_html, output_element, wait_for};
4+
use ssr_e2e_harness::{output_element, setup_ssr_page, wait_for};
55
use wasm_bindgen_test::*;
66

77
wasm_bindgen_test_configure!(run_in_browser);
@@ -10,8 +10,7 @@ const SERVER_BASE: &str = "http://127.0.0.1:8080";
1010

1111
#[wasm_bindgen_test]
1212
async fn hydration_succeeds() {
13-
let body_html = fetch_ssr_html(SERVER_BASE, "/").await;
14-
output_element().set_inner_html(&body_html);
13+
setup_ssr_page(SERVER_BASE, "/").await;
1514
yew::Renderer::<App>::with_root(output_element()).hydrate();
1615

1716
wait_for(

examples/ssr_router/tests/e2e.rs

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

3-
use std::time::Duration;
4-
53
use gloo::utils::document;
64
use ssr_e2e_harness::{
7-
fetch_count, fetch_ssr_html, output_element, patch_fetch, push_route, restore_fetch, wait_for,
5+
clear_resource_timings, fetch_ssr_html, navigate, output_element, resource_request_count,
6+
setup_ssr_page, wait_for,
87
};
98
use ssr_router::{App, AppProps, LINK_ENDPOINT};
109
use wasm_bindgen_test::*;
11-
use yew::platform::time::sleep;
10+
use yew::Renderer;
1211

1312
wasm_bindgen_test_configure!(run_in_browser);
1413

@@ -18,6 +17,15 @@ fn endpoint() -> String {
1817
format!("{SERVER_BASE}{LINK_ENDPOINT}")
1918
}
2019

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+
2129
fn get_title_text() -> Option<String> {
2230
document()
2331
.query_selector("h1.title")
@@ -26,58 +34,69 @@ fn get_title_text() -> Option<String> {
2634
.map(|el| el.text_content().unwrap_or_default())
2735
}
2836

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+
2956
#[wasm_bindgen_test]
30-
async fn hydrate_post_page() {
31-
let body_html = fetch_ssr_html(SERVER_BASE, "/posts/0").await;
57+
async fn ssr_hydration_and_client_navigation() {
58+
// -- Part 1: Direct SSR visit to /posts/0 triggers no fetch to /api/link --
3259

33-
patch_fetch();
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();
3464

35-
output_element().set_inner_html(&body_html);
36-
push_route("/posts/0");
37-
yew::Renderer::<App>::with_root_and_props(
38-
output_element(),
39-
AppProps {
40-
endpoint: endpoint().into(),
41-
},
42-
)
43-
.hydrate();
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();
4470

4571
wait_for(
4672
|| {
4773
let html = output_element().inner_html();
4874
html.contains("<h1 class=\"title\">") && !html.contains("Loading post...")
4975
},
5076
5000,
51-
"post page content",
77+
"post page content after SSR hydration",
5278
)
5379
.await;
5480

55-
let fc = fetch_count();
81+
let link_fetches = resource_request_count(LINK_ENDPOINT);
5682
let title = get_title_text();
5783

58-
restore_fetch();
59-
6084
assert_eq!(
61-
fc, 0,
62-
"direct visit to /posts/0 should not trigger any fetch (SSR provides data)"
85+
link_fetches, 0,
86+
"direct SSR visit to /posts/0 should not trigger any fetch to {LINK_ENDPOINT}"
6387
);
64-
let title = title.expect("h1.title should be present on the post page");
65-
assert!(!title.is_empty(), "post title should not be empty");
66-
}
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");
6790

68-
#[wasm_bindgen_test]
69-
async fn hydrate_posts_list() {
70-
let body_html = fetch_ssr_html(SERVER_BASE, "/posts").await;
91+
// -- Part 2: Navigate to /posts within the same app, then to /posts/0 --
7192

72-
output_element().set_inner_html(&body_html);
73-
push_route("/posts");
74-
yew::Renderer::<App>::with_root_and_props(
75-
output_element(),
76-
AppProps {
77-
endpoint: endpoint().into(),
78-
},
79-
)
80-
.hydrate();
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");
81100

82101
wait_for(
83102
|| {
@@ -86,41 +105,77 @@ async fn hydrate_posts_list() {
86105
.ok()
87106
.flatten()
88107
.is_some()
108+
&& get_title_text().as_deref() == Some("Posts")
89109
},
90-
10000,
91-
"post links to appear on /posts",
110+
15000,
111+
"posts list after client-side navigation to /posts",
92112
)
93113
.await;
94114

95-
// TODO: test client-side navigation triggers a fetch to /api/link.
96-
// Clicking <a> elements inside wasm-bindgen-test causes real browser navigation
97-
// (the test page navigates away from the test runner) because yew's event
98-
// delegation (capture-phase listener on the root element) does not intercept the
99-
// click. This may be because the root is the wasm-bindgen-test #output <pre>
100-
// element, or because headless Firefox handles synthetic clicks differently.
101-
// Needs further investigation with getEventListeners or a monkeypatched
102-
// addEventListener to confirm whether yew registers its capture handler on the
103-
// #output element during hydration.
104-
}
115+
clear_resource_timings();
105116

106-
#[wasm_bindgen_test]
107-
async fn hydrate_home() {
108-
let body_html = fetch_ssr_html(SERVER_BASE, "/").await;
117+
// HtmlElement.click() on <a> in headless Firefox triggers real browser
118+
// navigation, crashing the test runner. Yew's capture-phase event delegation
119+
// cannot intercept it in the wasm-bindgen-test context. Instead we use
120+
// gloo-history's BrowserHistory::push() which calls pushState and directly
121+
// invokes all registered history callbacks (the same codepath as yew-router's
122+
// <Link> component).
123+
navigate("/posts/0");
109124

110-
output_element().set_inner_html(&body_html);
111-
push_route("/");
112-
yew::Renderer::<App>::with_root_and_props(
113-
output_element(),
114-
AppProps {
115-
endpoint: endpoint().into(),
125+
wait_for(
126+
|| {
127+
// The post page renders a "by <author>" subtitle inside a hero section.
128+
// The posts LIST page subtitle says "All of our quality writing..."
129+
document()
130+
.query_selector("h2.subtitle")
131+
.ok()
132+
.flatten()
133+
.map(|el| el.text_content().unwrap_or_default())
134+
.is_some_and(|text| text.starts_with("by "))
116135
},
136+
15000,
137+
"post page content after client-side navigation to /posts/0",
117138
)
118-
.hydrate();
139+
.await;
140+
141+
// -- Part 3: Verify fetch happened and content matches SSR --
142+
143+
let nav_link_fetches = resource_request_count(LINK_ENDPOINT);
144+
let nav_title = get_title_text();
145+
let nav_body = post_body_text();
119146

120-
sleep(Duration::from_secs(2)).await;
121-
let html = output_element().inner_html();
122147
assert!(
123-
html.contains("Welcome"),
124-
"home page should have content after hydration"
148+
nav_link_fetches >= 1,
149+
"client-side navigation to /posts/0 should trigger at least one fetch to {LINK_ENDPOINT}, \
150+
got {nav_link_fetches}"
125151
);
152+
153+
let nav_title = nav_title.expect("h1.title should be present after client-side navigation");
154+
assert_eq!(
155+
ssr_title, nav_title,
156+
"post title should match between SSR and client-side navigation"
157+
);
158+
assert_eq!(
159+
ssr_body, nav_body,
160+
"post body should match between SSR and client-side navigation"
161+
);
162+
163+
app.destroy();
164+
output_element().set_inner_html("");
165+
}
166+
167+
#[wasm_bindgen_test]
168+
async fn hydrate_home() {
169+
setup_ssr_page(SERVER_BASE, "/").await;
170+
let app = make_renderer().hydrate();
171+
172+
wait_for(
173+
|| output_element().inner_html().contains("Welcome"),
174+
5000,
175+
"home page content after hydration",
176+
)
177+
.await;
178+
179+
app.destroy();
180+
output_element().set_inner_html("");
126181
}

tools/ssr-e2e-harness/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +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 }
1011
js-sys = { workspace = true }

0 commit comments

Comments
 (0)