Skip to main content

spider/features/
webdriver.rs

1use crate::configuration::Configuration;
2use crate::features::webdriver_args::get_browser_args;
3use crate::features::webdriver_common::{WebDriverBrowser, WebDriverConfig};
4use std::sync::Arc;
5use std::time::Duration;
6use thirtyfour::common::capabilities::desiredcapabilities::Capabilities;
7use thirtyfour::prelude::*;
8use tokio::task::JoinHandle;
9
10/// Stealth scripts from spider_fingerprint - cleans up automation markers.
11#[cfg(feature = "webdriver_stealth")]
12pub use spider_fingerprint::spoofs::{
13    spoof_device_memory, CLEANUP_CDP_MARKERS, HIDE_SELENIUM_MARKERS, HIDE_WEBDRIVER,
14};
15
16/// Legacy stealth script (fallback when spider_fingerprint not available).
17#[cfg(all(feature = "webdriver_stealth", not(feature = "serde")))]
18pub const STEALTH_SCRIPT: &str = r#"
19Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
20Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
21Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
22window.chrome = { runtime: {} };
23"#;
24
25/// WebDriver control tuple type alias (mirrors BrowserControl).
26pub type WebDriverControl = (Arc<WebDriver>, Option<JoinHandle<()>>, Option<String>);
27
28/// WebDriver controller with RAII cleanup (mirrors BrowserController).
29pub struct WebDriverController {
30    /// The WebDriver instance.
31    pub driver: WebDriverControl,
32    /// Whether the driver has been closed.
33    pub closed: bool,
34}
35
36impl WebDriverController {
37    /// Create a new WebDriver controller.
38    pub fn new(driver: WebDriverControl) -> Self {
39        Self {
40            driver,
41            closed: false,
42        }
43    }
44
45    /// Get a reference to the WebDriver.
46    pub fn driver(&self) -> &Arc<WebDriver> {
47        &self.driver.0
48    }
49
50    /// Dispose the WebDriver and close the session.
51    pub fn dispose(&mut self) {
52        if !self.closed {
53            self.closed = true;
54            if let Some(handler) = self.driver.1.take() {
55                handler.abort();
56            }
57        }
58    }
59}
60
61impl Drop for WebDriverController {
62    fn drop(&mut self) {
63        self.dispose();
64    }
65}
66
67/// Launch a WebDriver session with the provided configuration.
68pub async fn launch_driver(config: &Configuration) -> Option<WebDriverController> {
69    let webdriver_config = config.webdriver_config.as_ref()?;
70    launch_driver_base(webdriver_config, config).await
71}
72
73/// Launch a WebDriver session with the base configuration.
74pub async fn launch_driver_base(
75    webdriver_config: &WebDriverConfig,
76    config: &Configuration,
77) -> Option<WebDriverController> {
78    let server_url = &webdriver_config.server_url;
79
80    let caps = build_capabilities(webdriver_config, config).await?;
81
82    let mut attempts = 0;
83    let max_retries = 10;
84    let mut driver: Option<WebDriver> = None;
85
86    while attempts <= max_retries {
87        match WebDriver::new(server_url, caps.clone()).await {
88            Ok(d) => {
89                driver = Some(d);
90                break;
91            }
92            Err(err) => {
93                log::error!("WebDriver connection error: {:?}", err);
94                attempts += 1;
95                if attempts > max_retries {
96                    log::error!("Exceeded maximum retry attempts for WebDriver connection");
97                    break;
98                }
99                tokio::time::sleep(Duration::from_millis(500)).await;
100            }
101        }
102    }
103
104    let driver = driver?;
105    let driver_arc = Arc::new(driver);
106
107    // Set up viewport if configured
108    if let (Some(width), Some(height)) = (
109        webdriver_config.viewport_width,
110        webdriver_config.viewport_height,
111    ) {
112        if let Err(e) = driver_arc.set_window_rect(0, 0, width, height).await {
113            log::warn!("Failed to set viewport: {:?}", e);
114        }
115    }
116
117    // Set up timeouts
118    if let Some(timeout) = webdriver_config.timeout {
119        let timeouts = TimeoutConfiguration::new(Some(timeout), Some(timeout), Some(timeout));
120        if let Err(e) = driver_arc.update_timeouts(timeouts).await {
121            log::warn!("Failed to set timeouts: {:?}", e);
122        }
123    }
124
125    Some(WebDriverController::new((
126        driver_arc,
127        None,
128        Some(server_url.clone()),
129    )))
130}
131
132/// Build browser capabilities based on configuration.
133async fn build_capabilities(
134    webdriver_config: &WebDriverConfig,
135    config: &Configuration,
136) -> Option<Capabilities> {
137    match webdriver_config.browser {
138        WebDriverBrowser::Chrome => build_chrome_capabilities(webdriver_config, config).await,
139        WebDriverBrowser::Firefox => build_firefox_capabilities(webdriver_config, config).await,
140        WebDriverBrowser::Edge => build_edge_capabilities(webdriver_config, config).await,
141    }
142}
143
144/// Build Chrome capabilities.
145async fn build_chrome_capabilities(
146    webdriver_config: &WebDriverConfig,
147    config: &Configuration,
148) -> Option<Capabilities> {
149    let mut caps = DesiredCapabilities::chrome();
150
151    // Set accept insecure certs
152    if webdriver_config.accept_insecure_certs {
153        if let Err(e) = caps.accept_insecure_certs(true) {
154            log::warn!("Failed to set accept_insecure_certs: {:?}", e);
155        }
156    }
157
158    // Set page load strategy
159    if let Some(ref strategy) = webdriver_config.page_load_strategy {
160        let strategy = match strategy.as_str() {
161            "eager" => thirtyfour::PageLoadStrategy::Eager,
162            "none" => thirtyfour::PageLoadStrategy::None,
163            _ => thirtyfour::PageLoadStrategy::Normal,
164        };
165        if let Err(e) = caps.set_page_load_strategy(strategy) {
166            log::warn!("Failed to set page_load_strategy: {:?}", e);
167        }
168    }
169
170    // Collect all arguments
171    let mut args: Vec<String> = Vec::new();
172
173    // Add default browser args
174    let default_args = get_browser_args(&WebDriverBrowser::Chrome);
175    for arg in default_args {
176        args.push(arg.to_string());
177    }
178
179    // Add custom browser args
180    if let Some(ref custom_args) = webdriver_config.browser_args {
181        args.extend(custom_args.clone());
182    }
183
184    // Add headless argument if needed
185    if webdriver_config.headless && !args.iter().any(|a| a.contains("headless")) {
186        args.push("--headless".to_string());
187    }
188
189    // Add user agent
190    if let Some(ref ua) = webdriver_config.user_agent {
191        args.push(format!("--user-agent={}", ua));
192    } else if let Some(ref ua) = config.user_agent {
193        args.push(format!("--user-agent={}", ua));
194    }
195
196    // Add proxy
197    if let Some(ref proxy) = webdriver_config.proxy {
198        args.push(format!("--proxy-server={}", proxy));
199    }
200
201    // Add viewport
202    if let (Some(width), Some(height)) = (
203        webdriver_config.viewport_width,
204        webdriver_config.viewport_height,
205    ) {
206        args.push(format!("--window-size={},{}", width, height));
207    }
208
209    // Add all args to capabilities
210    for arg in args {
211        if let Err(e) = caps.add_arg(&arg) {
212            log::warn!("Failed to add Chrome arg '{}': {:?}", arg, e);
213        }
214    }
215
216    Some(caps.into())
217}
218
219/// Build Firefox capabilities.
220async fn build_firefox_capabilities(
221    webdriver_config: &WebDriverConfig,
222    _config: &Configuration,
223) -> Option<Capabilities> {
224    let mut caps = DesiredCapabilities::firefox();
225
226    // Set accept insecure certs
227    if webdriver_config.accept_insecure_certs {
228        if let Err(e) = caps.accept_insecure_certs(true) {
229            log::warn!("Failed to set accept_insecure_certs: {:?}", e);
230        }
231    }
232
233    // Set page load strategy
234    if let Some(ref strategy) = webdriver_config.page_load_strategy {
235        let strategy = match strategy.as_str() {
236            "eager" => thirtyfour::PageLoadStrategy::Eager,
237            "none" => thirtyfour::PageLoadStrategy::None,
238            _ => thirtyfour::PageLoadStrategy::Normal,
239        };
240        if let Err(e) = caps.set_page_load_strategy(strategy) {
241            log::warn!("Failed to set page_load_strategy: {:?}", e);
242        }
243    }
244
245    // Collect all arguments
246    let mut args: Vec<String> = Vec::new();
247
248    // Add default browser args
249    let default_args = get_browser_args(&WebDriverBrowser::Firefox);
250    for arg in default_args {
251        args.push(arg.to_string());
252    }
253
254    // Add custom browser args
255    if let Some(ref custom_args) = webdriver_config.browser_args {
256        args.extend(custom_args.clone());
257    }
258
259    // Add headless argument if needed
260    if webdriver_config.headless && !args.iter().any(|a| a.contains("headless")) {
261        args.push("-headless".to_string());
262    }
263
264    // Add all args to capabilities
265    for arg in args {
266        if let Err(e) = caps.add_arg(&arg) {
267            log::warn!("Failed to add Firefox arg '{}': {:?}", arg, e);
268        }
269    }
270
271    Some(caps.into())
272}
273
274/// Build Edge capabilities.
275async fn build_edge_capabilities(
276    webdriver_config: &WebDriverConfig,
277    config: &Configuration,
278) -> Option<Capabilities> {
279    let mut caps = DesiredCapabilities::edge();
280
281    // Set accept insecure certs
282    if webdriver_config.accept_insecure_certs {
283        if let Err(e) = caps.accept_insecure_certs(true) {
284            log::warn!("Failed to set accept_insecure_certs: {:?}", e);
285        }
286    }
287
288    // Set page load strategy
289    if let Some(ref strategy) = webdriver_config.page_load_strategy {
290        let strategy = match strategy.as_str() {
291            "eager" => thirtyfour::PageLoadStrategy::Eager,
292            "none" => thirtyfour::PageLoadStrategy::None,
293            _ => thirtyfour::PageLoadStrategy::Normal,
294        };
295        if let Err(e) = caps.set_page_load_strategy(strategy) {
296            log::warn!("Failed to set page_load_strategy: {:?}", e);
297        }
298    }
299
300    // Collect all arguments
301    let mut args: Vec<String> = Vec::new();
302
303    // Add default browser args
304    let default_args = get_browser_args(&WebDriverBrowser::Edge);
305    for arg in default_args {
306        args.push(arg.to_string());
307    }
308
309    // Add custom browser args
310    if let Some(ref custom_args) = webdriver_config.browser_args {
311        args.extend(custom_args.clone());
312    }
313
314    // Add headless argument if needed
315    if webdriver_config.headless && !args.iter().any(|a| a.contains("headless")) {
316        args.push("--headless".to_string());
317    }
318
319    // Add user agent
320    if let Some(ref ua) = webdriver_config.user_agent {
321        args.push(format!("--user-agent={}", ua));
322    } else if let Some(ref ua) = config.user_agent {
323        args.push(format!("--user-agent={}", ua));
324    }
325
326    // Add proxy
327    if let Some(ref proxy) = webdriver_config.proxy {
328        args.push(format!("--proxy-server={}", proxy));
329    }
330
331    // Add viewport
332    if let (Some(width), Some(height)) = (
333        webdriver_config.viewport_width,
334        webdriver_config.viewport_height,
335    ) {
336        args.push(format!("--window-size={},{}", width, height));
337    }
338
339    // Add all args to capabilities
340    for arg in args {
341        if let Err(e) = caps.add_arg(&arg) {
342            log::warn!("Failed to add Edge arg '{}': {:?}", arg, e);
343        }
344    }
345
346    Some(caps.into())
347}
348
349/// Setup WebDriver events and stealth mode.
350/// Injects spider_fingerprint stealth scripts to:
351/// - Hide navigator.webdriver property
352/// - Spoof navigator.deviceMemory (WebDriver doesn't have CDP for this)
353/// - Clean up CDP markers (cdc_, $cdc_)
354/// - Clean up Selenium markers
355#[cfg(feature = "webdriver_stealth")]
356pub async fn setup_driver_events(driver: &WebDriver, _config: &Configuration) {
357    // Inject webdriver hiding script
358    if let Err(e) = driver.execute(HIDE_WEBDRIVER, vec![]).await {
359        log::warn!("Failed to inject webdriver hiding script: {:?}", e);
360    }
361
362    // Spoof device memory (WebDriver doesn't have CDP emulation for this)
363    // Use realistic values: 4 or 8 GB for desktop
364    let device_memory_script = spoof_device_memory(8);
365    if let Err(e) = driver.execute(&device_memory_script, vec![]).await {
366        log::warn!("Failed to inject device memory script: {:?}", e);
367    }
368
369    // Clean up CDP markers (cdc_, $cdc_, etc.)
370    if let Err(e) = driver.execute(CLEANUP_CDP_MARKERS, vec![]).await {
371        log::warn!("Failed to inject CDP marker cleanup script: {:?}", e);
372    }
373
374    // Clean up Selenium-specific markers
375    if let Err(e) = driver.execute(HIDE_SELENIUM_MARKERS, vec![]).await {
376        log::warn!("Failed to inject Selenium marker cleanup script: {:?}", e);
377    }
378}
379
380/// Setup WebDriver events (no-op without stealth feature).
381#[cfg(not(feature = "webdriver_stealth"))]
382pub async fn setup_driver_events(_driver: &WebDriver, _config: &Configuration) {
383    // No stealth injection without the feature
384}
385
386/// Attempt to navigate to a URL.
387pub async fn attempt_navigation(
388    url: &str,
389    driver: &WebDriver,
390    timeout: &Option<Duration>,
391) -> Result<(), WebDriverError> {
392    let nav_future = driver.goto(url);
393
394    match timeout {
395        Some(t) => match tokio::time::timeout(*t, nav_future).await {
396            Ok(result) => result,
397            Err(_) => Err(WebDriverError::Timeout("Navigation timeout".to_string())),
398        },
399        None => nav_future.await,
400    }
401}
402
403/// Get the page content (HTML source).
404pub async fn get_page_content(driver: &WebDriver) -> Result<String, WebDriverError> {
405    driver.source().await
406}
407
408/// Get the current URL.
409pub async fn get_current_url(driver: &WebDriver) -> Result<String, WebDriverError> {
410    driver.current_url().await.map(|u| u.to_string())
411}
412
413/// Get the page title.
414pub async fn get_page_title(driver: &WebDriver) -> Result<String, WebDriverError> {
415    driver.title().await
416}
417
418/// Take a screenshot of the page.
419#[cfg(feature = "webdriver_screenshot")]
420pub async fn take_screenshot(driver: &WebDriver) -> Result<Vec<u8>, WebDriverError> {
421    driver.screenshot_as_png().await
422}
423
424/// Take a screenshot (stub without feature).
425#[cfg(not(feature = "webdriver_screenshot"))]
426pub async fn take_screenshot(_driver: &WebDriver) -> Result<Vec<u8>, WebDriverError> {
427    Err(WebDriverError::FatalError(
428        "Screenshot feature not enabled".to_string(),
429    ))
430}
431
432/// Execute JavaScript on the page and return the result as a JSON value.
433pub async fn execute_script(
434    driver: &WebDriver,
435    script: &str,
436) -> Result<serde_json::Value, WebDriverError> {
437    let result = driver.execute(script, vec![]).await?;
438    Ok(result.json().clone())
439}
440
441/// Wait for an element to be present.
442pub async fn wait_for_element(
443    driver: &WebDriver,
444    selector: &str,
445    timeout: Duration,
446) -> Result<WebElement, WebDriverError> {
447    driver
448        .query(By::Css(selector))
449        .wait(timeout, Duration::from_millis(100))
450        .first()
451        .await
452}
453
454/// Close the WebDriver session (consumes the driver).
455pub async fn close_driver(driver: WebDriver) {
456    if let Err(e) = driver.quit().await {
457        log::warn!("Failed to close WebDriver session: {:?}", e);
458    }
459}
460
461/// Get a random viewport for stealth purposes.
462#[cfg(feature = "real_browser")]
463pub fn get_random_webdriver_viewport() -> (u32, u32) {
464    use super::chrome_viewport::get_random_viewport;
465    let vp = get_random_viewport();
466    (vp.width, vp.height)
467}
468
469/// Get a default viewport.
470#[cfg(not(feature = "real_browser"))]
471pub fn get_random_webdriver_viewport() -> (u32, u32) {
472    (1920, 1080)
473}
474
475// ============================================================================
476// WebDriver Automation Support
477// ============================================================================
478
479use crate::features::chrome_common::WebAutomation;
480
481/// Run a single WebAutomation action on the WebDriver.
482pub async fn run_automation(driver: &WebDriver, action: &WebAutomation) -> bool {
483    let mut valid = false;
484
485    match action {
486        WebAutomation::Evaluate(js) => {
487            valid = driver.execute(js.as_str(), vec![]).await.is_ok();
488        }
489        WebAutomation::Click(selector) => {
490            if let Ok(ele) = driver.find(By::Css(selector)).await {
491                valid = ele.click().await.is_ok();
492            }
493        }
494        WebAutomation::ClickAll(selector) => {
495            if let Ok(eles) = driver.find_all(By::Css(selector)).await {
496                for ele in eles {
497                    valid = ele.click().await.is_ok();
498                }
499            }
500        }
501        WebAutomation::ClickPoint { x, y } => {
502            // WebDriver doesn't have direct click at coordinates, use JS
503            let js = format!("document.elementFromPoint({}, {})?.click()", x, y);
504            valid = driver.execute(&js, vec![]).await.is_ok();
505        }
506        WebAutomation::ClickHold { selector, hold_ms } => {
507            // Simulate with JS since WebDriver doesn't have native click-hold
508            let js = format!(
509                r#"
510                const el = document.querySelector('{}');
511                if (el) {{
512                    const evt = new MouseEvent('mousedown', {{ bubbles: true }});
513                    el.dispatchEvent(evt);
514                    await new Promise(r => setTimeout(r, {}));
515                    el.dispatchEvent(new MouseEvent('mouseup', {{ bubbles: true }}));
516                }}
517                "#,
518                selector.replace('\'', "\\'"),
519                hold_ms
520            );
521            valid = driver.execute(&js, vec![]).await.is_ok();
522        }
523        WebAutomation::ClickHoldPoint { x, y, hold_ms } => {
524            let js = format!(
525                r#"
526                const el = document.elementFromPoint({}, {});
527                if (el) {{
528                    el.dispatchEvent(new MouseEvent('mousedown', {{ bubbles: true }}));
529                    await new Promise(r => setTimeout(r, {}));
530                    el.dispatchEvent(new MouseEvent('mouseup', {{ bubbles: true }}));
531                }}
532                "#,
533                x, y, hold_ms
534            );
535            valid = driver.execute(&js, vec![]).await.is_ok();
536        }
537        WebAutomation::ClickDrag {
538            from,
539            to,
540            modifier: _,
541        } => {
542            // Simulate drag with JS
543            let js = format!(
544                r#"
545                const fromEl = document.querySelector('{}');
546                const toEl = document.querySelector('{}');
547                if (fromEl && toEl) {{
548                    const fromRect = fromEl.getBoundingClientRect();
549                    const toRect = toEl.getBoundingClientRect();
550                    fromEl.dispatchEvent(new MouseEvent('mousedown', {{ bubbles: true, clientX: fromRect.x, clientY: fromRect.y }}));
551                    toEl.dispatchEvent(new MouseEvent('mousemove', {{ bubbles: true, clientX: toRect.x, clientY: toRect.y }}));
552                    toEl.dispatchEvent(new MouseEvent('mouseup', {{ bubbles: true, clientX: toRect.x, clientY: toRect.y }}));
553                }}
554                "#,
555                from.replace('\'', "\\'"),
556                to.replace('\'', "\\'")
557            );
558            valid = driver.execute(&js, vec![]).await.is_ok();
559        }
560        WebAutomation::ClickDragPoint {
561            from_x,
562            from_y,
563            to_x,
564            to_y,
565            modifier: _,
566        } => {
567            let js = format!(
568                r#"
569                const fromEl = document.elementFromPoint({}, {});
570                const toEl = document.elementFromPoint({}, {});
571                if (fromEl) {{
572                    fromEl.dispatchEvent(new MouseEvent('mousedown', {{ bubbles: true, clientX: {}, clientY: {} }}));
573                    (toEl || fromEl).dispatchEvent(new MouseEvent('mousemove', {{ bubbles: true, clientX: {}, clientY: {} }}));
574                    (toEl || fromEl).dispatchEvent(new MouseEvent('mouseup', {{ bubbles: true, clientX: {}, clientY: {} }}));
575                }}
576                "#,
577                from_x, from_y, to_x, to_y, from_x, from_y, to_x, to_y, to_x, to_y
578            );
579            valid = driver.execute(&js, vec![]).await.is_ok();
580        }
581        WebAutomation::ClickAllClickable() => {
582            let clickable_selector =
583                "a, button, input[type='button'], input[type='submit'], [onclick], [role='button']";
584            if let Ok(eles) = driver.find_all(By::Css(clickable_selector)).await {
585                for ele in eles {
586                    let _ = ele.click().await;
587                    valid = true;
588                }
589            }
590        }
591        WebAutomation::Wait(ms) => {
592            tokio::time::sleep(Duration::from_millis(*ms)).await;
593            valid = true;
594        }
595        WebAutomation::WaitForNavigation => {
596            // Wait for page to finish loading
597            let js = r#"
598                return new Promise(resolve => {
599                    if (document.readyState === 'complete') {
600                        resolve(true);
601                    } else {
602                        window.addEventListener('load', () => resolve(true));
603                    }
604                });
605            "#;
606            valid = driver.execute(js, vec![]).await.is_ok();
607        }
608        WebAutomation::WaitForDom { selector, timeout } => {
609            let timeout_duration = Duration::from_millis(*timeout as u64);
610            if let Some(sel) = selector {
611                valid = driver
612                    .query(By::Css(sel))
613                    .wait(timeout_duration, Duration::from_millis(100))
614                    .first()
615                    .await
616                    .is_ok();
617            } else {
618                // Wait for DOM to be stable
619                tokio::time::sleep(timeout_duration).await;
620                valid = true;
621            }
622        }
623        WebAutomation::WaitFor(selector) => {
624            valid = driver
625                .query(By::Css(selector))
626                .wait(Duration::from_secs(60), Duration::from_millis(100))
627                .first()
628                .await
629                .is_ok();
630        }
631        WebAutomation::WaitForWithTimeout { selector, timeout } => {
632            valid = driver
633                .query(By::Css(selector))
634                .wait(Duration::from_millis(*timeout), Duration::from_millis(100))
635                .first()
636                .await
637                .is_ok();
638        }
639        WebAutomation::WaitForAndClick(selector) => {
640            if let Ok(ele) = driver
641                .query(By::Css(selector))
642                .wait(Duration::from_secs(60), Duration::from_millis(100))
643                .first()
644                .await
645            {
646                valid = ele.click().await.is_ok();
647            }
648        }
649        WebAutomation::ScrollX(px) => {
650            let js = format!("window.scrollBy({}, 0)", px);
651            valid = driver.execute(&js, vec![]).await.is_ok();
652        }
653        WebAutomation::ScrollY(px) => {
654            let js = format!("window.scrollBy(0, {})", px);
655            valid = driver.execute(&js, vec![]).await.is_ok();
656        }
657        WebAutomation::InfiniteScroll(duration) => {
658            let timeout = (*duration).min(300); // Cap at 5 minutes
659            let js = format!(
660                r#"
661                const endTime = Date.now() + {} * 1000;
662                const scroll = () => {{
663                    window.scrollBy(0, window.innerHeight);
664                    if (Date.now() < endTime) {{
665                        setTimeout(scroll, 500);
666                    }}
667                }};
668                scroll();
669                "#,
670                timeout
671            );
672            valid = driver.execute(&js, vec![]).await.is_ok();
673            // Wait for the scroll to complete
674            tokio::time::sleep(Duration::from_secs(timeout as u64)).await;
675        }
676        WebAutomation::Fill { selector, value } => {
677            if let Ok(ele) = driver.find(By::Css(selector)).await {
678                // Clear and type
679                if ele.clear().await.is_ok() {
680                    valid = ele.send_keys(value).await.is_ok();
681                }
682            }
683        }
684        WebAutomation::Type { value, modifier: _ } => {
685            // Type into the active element
686            let js = format!(
687                r#"
688                const el = document.activeElement;
689                if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable)) {{
690                    el.value = (el.value || '') + '{}';
691                    el.dispatchEvent(new Event('input', {{ bubbles: true }}));
692                }}
693                "#,
694                value.replace('\'', "\\'").replace('\n', "\\n")
695            );
696            valid = driver.execute(&js, vec![]).await.is_ok();
697        }
698        WebAutomation::Screenshot {
699            full_page: _,
700            omit_background: _,
701            output,
702        } => {
703            #[cfg(feature = "webdriver_screenshot")]
704            {
705                if let Ok(png_data) = driver.screenshot_as_png().await {
706                    valid = crate::utils::uring_fs::write_file(output.clone(), png_data)
707                        .await
708                        .is_ok();
709                }
710            }
711            #[cfg(not(feature = "webdriver_screenshot"))]
712            {
713                let _ = output;
714                log::warn!("Screenshot feature not enabled");
715            }
716        }
717        WebAutomation::ValidateChain => {
718            // This is a control flow marker, always returns current valid state
719            valid = true;
720        }
721    }
722
723    valid
724}
725
726/// Run a list of WebAutomation actions on the WebDriver.
727pub async fn run_automation_scripts(driver: &WebDriver, scripts: &[WebAutomation]) -> bool {
728    let mut valid = false;
729
730    for script in scripts {
731        if script == &WebAutomation::ValidateChain && !valid {
732            break;
733        }
734        match tokio::time::timeout(Duration::from_secs(60), run_automation(driver, script)).await {
735            Ok(result) => valid = result,
736            Err(_) => {
737                log::warn!("Automation script timed out: {:?}", script.name());
738                valid = false;
739            }
740        }
741    }
742
743    valid
744}
745
746/// Run execution scripts (JavaScript) for a specific URL.
747pub async fn run_execution_scripts(
748    driver: &WebDriver,
749    url: &str,
750    execution_scripts: &Option<crate::features::chrome_common::ExecutionScripts>,
751) {
752    if let Some(scripts) = execution_scripts {
753        if let Some(js) = scripts.search(url) {
754            if let Err(e) = driver.execute(js.as_str(), vec![]).await {
755                log::warn!("Failed to execute script for {}: {:?}", url, e);
756            }
757        }
758    }
759}
760
761/// Run automation scripts for a specific URL.
762pub async fn run_url_automation_scripts(
763    driver: &WebDriver,
764    url: &str,
765    automation_scripts: &Option<crate::features::chrome_common::AutomationScripts>,
766) -> bool {
767    if let Some(scripts) = automation_scripts {
768        if let Some(actions) = scripts.search(url) {
769            return run_automation_scripts(driver, actions).await;
770        }
771    }
772    true
773}