@@ -1047,8 +1047,12 @@ impl AsyncStaticExporter {
10471047
10481048 async fn extract ( & mut self , html_content : & str , plot : & PlotData < ' _ > ) -> Result < String > {
10491049 let caps = self . build_webdriver_caps ( ) ?;
1050- debug ! ( "Use WebDriver and headless browser to export static plot" ) ;
1050+ debug ! (
1051+ "Use WebDriver and headless browser to export static plot (offline_mode={}, port={})" ,
1052+ self . offline_mode, self . webdriver_port
1053+ ) ;
10511054 let webdriver_url = format ! ( "{}:{}" , self . webdriver_url, self . webdriver_port) ;
1055+ debug ! ( "Connecting to WebDriver at {webdriver_url}" ) ;
10521056
10531057 // Reuse existing client or create new one
10541058 let client = if let Some ( ref client) = self . webdriver_client {
@@ -1079,6 +1083,70 @@ impl AsyncStaticExporter {
10791083 // Open the HTML
10801084 client. goto ( & url) . await ?;
10811085
1086+ // Ensure DOM is ready and required elements/scripts are available (Windows CI race)
1087+ {
1088+ let start = std:: time:: Instant :: now ( ) ;
1089+ let timeout = std:: time:: Duration :: from_secs ( 10 ) ;
1090+ loop {
1091+ let state = client
1092+ . execute ( "return document.readyState;" , vec ! [ ] )
1093+ . await
1094+ . unwrap_or_else ( |_| serde_json:: Value :: Null ) ;
1095+ if state. as_str ( ) . map ( |s| s == "complete" ) . unwrap_or ( false ) {
1096+ break ;
1097+ }
1098+ if start. elapsed ( ) > timeout {
1099+ return Err ( anyhow ! (
1100+ "Timeout waiting for document.readyState === 'complete'"
1101+ ) ) ;
1102+ }
1103+ tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( 50 ) ) . await ;
1104+ }
1105+ }
1106+
1107+ // Wait for Plotly container element
1108+ {
1109+ let start = std:: time:: Instant :: now ( ) ;
1110+ let timeout = std:: time:: Duration :: from_secs ( 10 ) ;
1111+ loop {
1112+ let has_el = client
1113+ . execute (
1114+ "return !!document.getElementById('plotly-html-element');" ,
1115+ vec ! [ ] ,
1116+ )
1117+ . await
1118+ . unwrap_or_else ( |_| serde_json:: Value :: Bool ( false ) ) ;
1119+ if has_el. as_bool ( ) . unwrap_or ( false ) {
1120+ break ;
1121+ }
1122+ if start. elapsed ( ) > timeout {
1123+ return Err ( anyhow ! (
1124+ "Timeout waiting for #plotly-html-element to appear in DOM"
1125+ ) ) ;
1126+ }
1127+ tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( 50 ) ) . await ;
1128+ }
1129+ }
1130+
1131+ // In online mode, ensure Plotly is loaded
1132+ if !self . offline_mode {
1133+ let start = std:: time:: Instant :: now ( ) ;
1134+ let timeout = std:: time:: Duration :: from_secs ( 15 ) ;
1135+ loop {
1136+ let has_plotly = client
1137+ . execute ( "return !!window.Plotly;" , vec ! [ ] )
1138+ . await
1139+ . unwrap_or_else ( |_| serde_json:: Value :: Bool ( false ) ) ;
1140+ if has_plotly. as_bool ( ) . unwrap_or ( false ) {
1141+ break ;
1142+ }
1143+ if start. elapsed ( ) > timeout {
1144+ return Err ( anyhow ! ( "Timeout waiting for Plotly library to load" ) ) ;
1145+ }
1146+ tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( 100 ) ) . await ;
1147+ }
1148+ }
1149+
10821150 let ( js_script, args) = match plot. format {
10831151 ImageFormat :: PDF => {
10841152 // Always use SVG for PDF export
@@ -1191,20 +1259,30 @@ impl AsyncStaticExporter {
11911259
11921260#[ cfg( test) ]
11931261mod tests {
1194- use std:: path:: PathBuf ;
1195- use std:: sync:: atomic:: { AtomicU32 , Ordering } ;
1196-
11971262 use super :: * ;
1263+ use std:: path:: PathBuf ;
11981264
11991265 fn init ( ) {
12001266 let _ = env_logger:: try_init ( ) ;
12011267 }
12021268
12031269 // Helper to generate unique ports for parallel tests
1204- static PORT_COUNTER : AtomicU32 = AtomicU32 :: new ( 4444 ) ;
1205-
12061270 fn get_unique_port ( ) -> u32 {
1207- PORT_COUNTER . fetch_add ( 1 , Ordering :: SeqCst )
1271+ use std:: sync:: atomic:: { AtomicU32 , Ordering } ;
1272+ static PORT_COUNTER : AtomicU32 = AtomicU32 :: new ( 4444 ) ;
1273+
1274+ // Before we used this counter to generate unique ports.
1275+ // >>> PORT_COUNTER.fetch_add(1, Ordering::SeqCst)
1276+ // However, sometimes the webdriver process is not stopped immediately
1277+ // and we get port conflicts.
1278+ // We try to give some time for other webdriver processes to stop so that we don't
1279+ // get port conflicts.
1280+ loop {
1281+ let p = PORT_COUNTER . fetch_add ( 1 , Ordering :: SeqCst ) ;
1282+ if !webdriver:: WebDriver :: is_webdriver_running ( p) {
1283+ return p;
1284+ }
1285+ }
12081286 }
12091287
12101288 fn create_test_plot ( ) -> serde_json:: Value {
@@ -1370,7 +1448,7 @@ mod tests {
13701448
13711449 let mut exporter = StaticExporterBuilder :: default ( )
13721450 . spawn_webdriver ( true )
1373- . webdriver_port ( get_unique_port ( ) )
1451+ . webdriver_port ( 5444 )
13741452 . build_async ( )
13751453 . unwrap ( ) ;
13761454
0 commit comments