@@ -266,7 +266,7 @@ use std::{println as error, println as warn, println as debug};
266266
267267use anyhow:: { anyhow, Context , Result } ;
268268use base64:: { engine:: general_purpose, Engine as _} ;
269- use fantoccini:: { wd:: Capabilities , ClientBuilder } ;
269+ use fantoccini:: { wd:: Capabilities , Client , ClientBuilder } ;
270270#[ cfg( not( test) ) ]
271271use log:: { debug, error, warn} ;
272272use serde:: Serialize ;
@@ -469,10 +469,15 @@ struct PlotData<'a> {
469469/// - Browser capabilities: Default Chrome/Firefox headless options
470470/// - Automatic WebDriver detection and connection reuse
471471pub struct StaticExporterBuilder {
472+ /// WebDriver server port (default: 4444)
472473 webdriver_port : u32 ,
474+ /// WebDriver server base URL (default: "http://localhost")
473475 webdriver_url : String ,
476+ /// Auto-spawn WebDriver if not running (default: true)
474477 spawn_webdriver : bool ,
478+ /// Use bundled JS libraries instead of CDN (default: false)
475479 offline_mode : bool ,
480+ /// Browser command-line flags (e.g., "--headless", "--no-sandbox")
476481 webdriver_browser_caps : Vec < String > ,
477482}
478483
@@ -675,6 +680,7 @@ impl StaticExporterBuilder {
675680 offline_mode : self . offline_mode ,
676681 webdriver_browser_caps : self . webdriver_browser_caps . clone ( ) ,
677682 runtime,
683+ webdriver_client : None ,
678684 } )
679685 }
680686
@@ -737,12 +743,26 @@ impl StaticExporterBuilder {
737743/// - Offline mode support
738744/// - Automatic WebDriver management
739745pub struct StaticExporter {
746+ /// WebDriver server port (default: 4444)
740747 webdriver_port : u32 ,
748+
749+ /// WebDriver server base URL (default: "http://localhost")
741750 webdriver_url : String ,
751+
752+ /// WebDriver process manager for spawning and cleanup
742753 webdriver : WebDriver ,
754+
755+ /// Use bundled JS libraries instead of CDN
743756 offline_mode : bool ,
757+
758+ /// Browser command-line flags (e.g., "--headless", "--no-sandbox")
744759 webdriver_browser_caps : Vec < String > ,
760+
761+ /// Tokio runtime for async operations
745762 runtime : std:: sync:: Arc < tokio:: runtime:: Runtime > ,
763+
764+ /// Cached WebDriver client for session reuse
765+ webdriver_client : Option < Client > ,
746766}
747767
748768impl Drop for StaticExporter {
@@ -757,6 +777,16 @@ impl Drop for StaticExporter {
757777 /// - Leaves externally managed WebDriver sessions running
758778 /// - Logs errors but doesn't panic if cleanup fails
759779 fn drop ( & mut self ) {
780+ // Close the WebDriver client if it exists
781+ if let Some ( client) = self . webdriver_client . take ( ) {
782+ let runtime = self . runtime . clone ( ) ;
783+ runtime. block_on ( async {
784+ if let Err ( e) = client. close ( ) . await {
785+ error ! ( "Failed to close WebDriver client: {e}" ) ;
786+ }
787+ } ) ;
788+ }
789+
760790 // Stop the WebDriver process
761791 if let Err ( e) = self . webdriver . stop ( ) {
762792 error ! ( "Failed to stop WebDriver: {e}" ) ;
@@ -941,11 +971,20 @@ impl StaticExporter {
941971 debug ! ( "Use WebDriver and headless browser to export static plot" ) ;
942972 let webdriver_url = format ! ( "{}:{}" , self . webdriver_url, self . webdriver_port, ) ;
943973
944- let client = ClientBuilder :: native ( )
945- . capabilities ( caps)
946- . connect ( & webdriver_url)
947- . await
948- . with_context ( || "WebDriver session errror" ) ?;
974+ // Reuse existing client or create new one
975+ let client = if let Some ( ref client) = self . webdriver_client {
976+ debug ! ( "Reusing existing WebDriver session" ) ;
977+ client. clone ( )
978+ } else {
979+ debug ! ( "Creating new WebDriver session" ) ;
980+ let new_client = ClientBuilder :: native ( )
981+ . capabilities ( caps)
982+ . connect ( & webdriver_url)
983+ . await
984+ . with_context ( || "WebDriver session error" ) ?;
985+ self . webdriver_client = Some ( new_client. clone ( ) ) ;
986+ new_client
987+ } ;
949988
950989 // URL-encode the HTML
951990 let data_uri = format ! ( "data:text/html,{}" , encode( data_uri) ) ;
@@ -978,7 +1017,8 @@ impl StaticExporter {
9781017
9791018 let data = client. execute_async ( js_script, args) . await ?;
9801019
981- client. close ( ) . await ?;
1020+ // Don't close the client - keep it for reuse
1021+ // client.close().await?;
9821022
9831023 let src = data. as_str ( ) . ok_or ( anyhow ! (
9841024 "Failed to execute Plotly.toImage in browser session"
@@ -1282,46 +1322,50 @@ mod tests {
12821322 init ( ) ;
12831323 let test_plot = create_test_plot ( ) ;
12841324
1325+ // Use a unique port to test actual WebDriver process reuse
1326+ let test_port = get_unique_port ( ) ;
1327+
12851328 // Create first exporter - this should spawn a new WebDriver
12861329 let mut export1 = StaticExporterBuilder :: default ( )
12871330 . spawn_webdriver ( true )
1288- . webdriver_port ( get_unique_port ( ) )
1331+ . webdriver_port ( test_port )
12891332 . build ( )
12901333 . unwrap ( ) ;
12911334
12921335 // Export first image
1293- let dst1 = PathBuf :: from ( "session_reuse_1 .png" ) ;
1336+ let dst1 = PathBuf :: from ( "process_reuse_1 .png" ) ;
12941337 export1
12951338 . write_fig ( dst1. as_path ( ) , & test_plot, ImageFormat :: PNG , 800 , 600 , 1.0 )
12961339 . unwrap ( ) ;
12971340 assert ! ( dst1. exists( ) ) ;
12981341 assert ! ( std:: fs:: remove_file( dst1. as_path( ) ) . is_ok( ) ) ;
12991342
13001343 // Create second exporter on the same port - this should connect to existing
1301- // WebDriver
1344+ // WebDriver process (but create a new session)
13021345 let mut export2 = StaticExporterBuilder :: default ( )
13031346 . spawn_webdriver ( true )
1304- . webdriver_port ( get_unique_port ( ) )
1347+ . webdriver_port ( test_port )
13051348 . build ( )
13061349 . unwrap ( ) ;
13071350
1308- // Export second image using the same WebDriver session
1309- let dst2 = PathBuf :: from ( "session_reuse_2 .png" ) ;
1351+ // Export second image using a new session on the same WebDriver process
1352+ let dst2 = PathBuf :: from ( "process_reuse_2 .png" ) ;
13101353 export2
13111354 . write_fig ( dst2. as_path ( ) , & test_plot, ImageFormat :: PNG , 800 , 600 , 1.0 )
13121355 . unwrap ( ) ;
13131356 assert ! ( dst2. exists( ) ) ;
13141357 assert ! ( std:: fs:: remove_file( dst2. as_path( ) ) . is_ok( ) ) ;
13151358
13161359 // Create third exporter on the same port - should also connect to existing
1360+ // WebDriver process
13171361 let mut export3 = StaticExporterBuilder :: default ( )
13181362 . spawn_webdriver ( true )
1319- . webdriver_port ( get_unique_port ( ) )
1363+ . webdriver_port ( test_port )
13201364 . build ( )
13211365 . unwrap ( ) ;
13221366
1323- // Export third image
1324- let dst3 = PathBuf :: from ( "session_reuse_3 .png" ) ;
1367+ // Export third image using another new session on the same WebDriver process
1368+ let dst3 = PathBuf :: from ( "process_reuse_3 .png" ) ;
13251369 export3
13261370 . write_fig ( dst3. as_path ( ) , & test_plot, ImageFormat :: PNG , 800 , 600 , 1.0 )
13271371 . unwrap ( ) ;
0 commit comments