11#![ cfg( all( target_arch = "wasm32" , not( target_os = "wasi" ) ) ) ]
22
3- use std:: time:: Duration ;
4-
5- use function_router:: App ;
63use 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 } ;
89use wasm_bindgen_test:: * ;
9- use yew:: platform :: time :: sleep ;
10+ use yew:: Renderer ;
1011
1112wasm_bindgen_test_configure ! ( run_in_browser) ;
1213
1314const 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+
1529fn 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]
62160async 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}
0 commit comments