Skip to main content

browser_commander/interactions/
fill.rs

1//! Fill operations for browser automation.
2//!
3//! This module provides utilities for filling form elements
4//! with verification support.
5
6use crate::core::constants::TIMING;
7use crate::core::engine::{EngineAdapter, EngineError, FillVerificationResult};
8use crate::core::navigation::is_navigation_error;
9use crate::elements::content::is_element_empty;
10use crate::interactions::click::click_element;
11use crate::interactions::scroll::{scroll_into_view_if_needed, ScrollOptions};
12use std::time::{Duration, Instant};
13
14/// Options for fill operations.
15#[derive(Debug, Clone)]
16pub struct FillOptions {
17    /// Whether to scroll the element into view before filling.
18    pub scroll_into_view: bool,
19    /// Whether to simulate typing (character by character).
20    pub simulate_typing: bool,
21    /// Only fill if the element is empty.
22    pub check_empty: bool,
23    /// Whether to verify the fill operation.
24    pub verify: bool,
25    /// Timeout for verification.
26    pub verification_timeout: Duration,
27    /// Interval between verification retries.
28    pub verification_retry_interval: Duration,
29    /// Timeout for the overall operation.
30    pub timeout: Duration,
31}
32
33impl Default for FillOptions {
34    fn default() -> Self {
35        Self {
36            scroll_into_view: true,
37            simulate_typing: true,
38            check_empty: true,
39            verify: true,
40            verification_timeout: TIMING.verification_timeout,
41            verification_retry_interval: TIMING.verification_retry_interval,
42            timeout: TIMING.default_timeout,
43        }
44    }
45}
46
47/// Result of a fill operation.
48#[derive(Debug, Clone)]
49pub struct FillResult {
50    /// Whether the fill was performed.
51    pub filled: bool,
52    /// Whether the fill was verified as successful.
53    pub verified: bool,
54    /// Whether the fill was skipped (element had content).
55    pub skipped: bool,
56    /// The actual value after filling.
57    pub actual_value: Option<String>,
58}
59
60impl FillResult {
61    /// Create a successful fill result.
62    pub fn success(actual_value: String) -> Self {
63        Self {
64            filled: true,
65            verified: true,
66            skipped: false,
67            actual_value: Some(actual_value),
68        }
69    }
70
71    /// Create a result indicating fill was skipped.
72    pub fn skipped(actual_value: String) -> Self {
73        Self {
74            filled: false,
75            verified: false,
76            skipped: true,
77            actual_value: Some(actual_value),
78        }
79    }
80
81    /// Create a failed fill result.
82    pub fn failed() -> Self {
83        Self {
84            filled: false,
85            verified: false,
86            skipped: false,
87            actual_value: None,
88        }
89    }
90}
91
92/// Verify a fill operation by checking the element's value.
93///
94/// # Arguments
95///
96/// * `adapter` - The engine adapter to use
97/// * `selector` - The CSS selector for the element
98/// * `expected_text` - The expected text value
99/// * `options` - Fill options
100///
101/// # Returns
102///
103/// The verification result
104pub async fn verify_fill(
105    adapter: &dyn EngineAdapter,
106    selector: &str,
107    expected_text: &str,
108    options: &FillOptions,
109) -> Result<FillVerificationResult, EngineError> {
110    let start_time = Instant::now();
111    let mut attempts = 0u32;
112
113    while start_time.elapsed() < options.verification_timeout {
114        attempts += 1;
115
116        let actual_value = match adapter.input_value(selector).await {
117            Ok(Some(value)) => value,
118            Ok(None) => String::new(),
119            Err(e) if is_navigation_error(&e.to_string()) => {
120                return Ok(FillVerificationResult {
121                    verified: false,
122                    actual_value: String::new(),
123                    attempts,
124                });
125            }
126            Err(e) => return Err(e),
127        };
128
129        // Verify that the value contains the expected text
130        let verified = actual_value == expected_text || actual_value.contains(expected_text);
131
132        if verified {
133            return Ok(FillVerificationResult {
134                verified: true,
135                actual_value,
136                attempts,
137            });
138        }
139
140        tokio::time::sleep(options.verification_retry_interval).await;
141    }
142
143    // Final check
144    let actual_value = adapter.input_value(selector).await?.unwrap_or_default();
145
146    Ok(FillVerificationResult {
147        verified: false,
148        actual_value,
149        attempts,
150    })
151}
152
153/// Fill an input element with text (low-level operation).
154///
155/// # Arguments
156///
157/// * `adapter` - The engine adapter to use
158/// * `selector` - The CSS selector for the element
159/// * `text` - The text to fill
160/// * `options` - Fill options
161///
162/// # Returns
163///
164/// The result of the fill operation
165pub async fn perform_fill(
166    adapter: &dyn EngineAdapter,
167    selector: &str,
168    text: &str,
169    options: &FillOptions,
170) -> Result<FillResult, EngineError> {
171    // Perform the fill operation
172    if options.simulate_typing {
173        match adapter.type_text(selector, text).await {
174            Ok(_) => {}
175            Err(e) if is_navigation_error(&e.to_string()) => {
176                return Ok(FillResult::failed());
177            }
178            Err(e) => return Err(e),
179        }
180    } else {
181        match adapter.fill(selector, text).await {
182            Ok(_) => {}
183            Err(e) if is_navigation_error(&e.to_string()) => {
184                return Ok(FillResult::failed());
185            }
186            Err(e) => return Err(e),
187        }
188    }
189
190    // Verify if requested
191    if options.verify {
192        let verification = verify_fill(adapter, selector, text, options).await?;
193        Ok(FillResult {
194            filled: true,
195            verified: verification.verified,
196            skipped: false,
197            actual_value: Some(verification.actual_value),
198        })
199    } else {
200        Ok(FillResult {
201            filled: true,
202            verified: true,
203            skipped: false,
204            actual_value: None,
205        })
206    }
207}
208
209/// Fill a text area with text (high-level with checks and scrolling).
210///
211/// This function handles:
212/// - Checking if the element is empty (if requested)
213/// - Scrolling the element into view
214/// - Clicking to focus
215/// - Filling the text
216/// - Verifying the fill
217///
218/// # Arguments
219///
220/// * `adapter` - The engine adapter to use
221/// * `selector` - The CSS selector for the element
222/// * `text` - The text to fill
223/// * `options` - Fill options
224///
225/// # Returns
226///
227/// The result of the fill operation
228pub async fn fill_text_area(
229    adapter: &dyn EngineAdapter,
230    selector: &str,
231    text: &str,
232    options: &FillOptions,
233) -> Result<FillResult, EngineError> {
234    // Check if empty (if requested)
235    if options.check_empty {
236        let is_empty = is_element_empty(adapter, selector).await?;
237        if !is_empty {
238            let current_value = adapter.input_value(selector).await?.unwrap_or_default();
239            return Ok(FillResult::skipped(current_value));
240        }
241    }
242
243    // Scroll into view if requested
244    if options.scroll_into_view {
245        let scroll_options = ScrollOptions::default();
246        match scroll_into_view_if_needed(adapter, selector, &scroll_options).await {
247            Ok(_) => {}
248            Err(e) if is_navigation_error(&e.to_string()) => {
249                return Ok(FillResult::failed());
250            }
251            Err(e) => return Err(e),
252        }
253    }
254
255    // Click to focus
256    let click_options = crate::interactions::click::ClickOptions {
257        scroll_into_view: false, // Already scrolled
258        verify: false,           // Don't need to verify the focus click
259        ..Default::default()
260    };
261
262    match click_element(adapter, selector, &click_options).await {
263        Ok(_) => {}
264        Err(e) if is_navigation_error(&e.to_string()) => {
265            return Ok(FillResult::failed());
266        }
267        Err(e) => return Err(e),
268    }
269
270    // Perform the fill
271    perform_fill(adapter, selector, text, options).await
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn fill_options_default() {
280        let options = FillOptions::default();
281        assert!(options.scroll_into_view);
282        assert!(options.simulate_typing);
283        assert!(options.check_empty);
284        assert!(options.verify);
285    }
286
287    #[test]
288    fn fill_result_success() {
289        let result = FillResult::success("filled text".to_string());
290        assert!(result.filled);
291        assert!(result.verified);
292        assert!(!result.skipped);
293        assert_eq!(result.actual_value, Some("filled text".to_string()));
294    }
295
296    #[test]
297    fn fill_result_skipped() {
298        let result = FillResult::skipped("existing text".to_string());
299        assert!(!result.filled);
300        assert!(!result.verified);
301        assert!(result.skipped);
302        assert_eq!(result.actual_value, Some("existing text".to_string()));
303    }
304
305    #[test]
306    fn fill_result_failed() {
307        let result = FillResult::failed();
308        assert!(!result.filled);
309        assert!(!result.verified);
310        assert!(!result.skipped);
311        assert!(result.actual_value.is_none());
312    }
313}