use crate::core::constants::TIMING;
use crate::core::engine::{EngineAdapter, EngineError, FillVerificationResult};
use crate::core::navigation::is_navigation_error;
use crate::elements::content::is_element_empty;
use crate::interactions::click::click_element;
use crate::interactions::scroll::{scroll_into_view_if_needed, ScrollOptions};
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct FillOptions {
pub scroll_into_view: bool,
pub simulate_typing: bool,
pub check_empty: bool,
pub verify: bool,
pub verification_timeout: Duration,
pub verification_retry_interval: Duration,
pub timeout: Duration,
}
impl Default for FillOptions {
fn default() -> Self {
Self {
scroll_into_view: true,
simulate_typing: true,
check_empty: true,
verify: true,
verification_timeout: TIMING.verification_timeout,
verification_retry_interval: TIMING.verification_retry_interval,
timeout: TIMING.default_timeout,
}
}
}
#[derive(Debug, Clone)]
pub struct FillResult {
pub filled: bool,
pub verified: bool,
pub skipped: bool,
pub actual_value: Option<String>,
}
impl FillResult {
pub fn success(actual_value: String) -> Self {
Self {
filled: true,
verified: true,
skipped: false,
actual_value: Some(actual_value),
}
}
pub fn skipped(actual_value: String) -> Self {
Self {
filled: false,
verified: false,
skipped: true,
actual_value: Some(actual_value),
}
}
pub fn failed() -> Self {
Self {
filled: false,
verified: false,
skipped: false,
actual_value: None,
}
}
}
pub async fn verify_fill(
adapter: &dyn EngineAdapter,
selector: &str,
expected_text: &str,
options: &FillOptions,
) -> Result<FillVerificationResult, EngineError> {
let start_time = Instant::now();
let mut attempts = 0u32;
while start_time.elapsed() < options.verification_timeout {
attempts += 1;
let actual_value = match adapter.input_value(selector).await {
Ok(Some(value)) => value,
Ok(None) => String::new(),
Err(e) if is_navigation_error(&e.to_string()) => {
return Ok(FillVerificationResult {
verified: false,
actual_value: String::new(),
attempts,
});
}
Err(e) => return Err(e),
};
let verified = actual_value == expected_text || actual_value.contains(expected_text);
if verified {
return Ok(FillVerificationResult {
verified: true,
actual_value,
attempts,
});
}
tokio::time::sleep(options.verification_retry_interval).await;
}
let actual_value = adapter.input_value(selector).await?.unwrap_or_default();
Ok(FillVerificationResult {
verified: false,
actual_value,
attempts,
})
}
pub async fn perform_fill(
adapter: &dyn EngineAdapter,
selector: &str,
text: &str,
options: &FillOptions,
) -> Result<FillResult, EngineError> {
if options.simulate_typing {
match adapter.type_text(selector, text).await {
Ok(_) => {}
Err(e) if is_navigation_error(&e.to_string()) => {
return Ok(FillResult::failed());
}
Err(e) => return Err(e),
}
} else {
match adapter.fill(selector, text).await {
Ok(_) => {}
Err(e) if is_navigation_error(&e.to_string()) => {
return Ok(FillResult::failed());
}
Err(e) => return Err(e),
}
}
if options.verify {
let verification = verify_fill(adapter, selector, text, options).await?;
Ok(FillResult {
filled: true,
verified: verification.verified,
skipped: false,
actual_value: Some(verification.actual_value),
})
} else {
Ok(FillResult {
filled: true,
verified: true,
skipped: false,
actual_value: None,
})
}
}
pub async fn fill_text_area(
adapter: &dyn EngineAdapter,
selector: &str,
text: &str,
options: &FillOptions,
) -> Result<FillResult, EngineError> {
if options.check_empty {
let is_empty = is_element_empty(adapter, selector).await?;
if !is_empty {
let current_value = adapter.input_value(selector).await?.unwrap_or_default();
return Ok(FillResult::skipped(current_value));
}
}
if options.scroll_into_view {
let scroll_options = ScrollOptions::default();
match scroll_into_view_if_needed(adapter, selector, &scroll_options).await {
Ok(_) => {}
Err(e) if is_navigation_error(&e.to_string()) => {
return Ok(FillResult::failed());
}
Err(e) => return Err(e),
}
}
let click_options = crate::interactions::click::ClickOptions {
scroll_into_view: false, verify: false, ..Default::default()
};
match click_element(adapter, selector, &click_options).await {
Ok(_) => {}
Err(e) if is_navigation_error(&e.to_string()) => {
return Ok(FillResult::failed());
}
Err(e) => return Err(e),
}
perform_fill(adapter, selector, text, options).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fill_options_default() {
let options = FillOptions::default();
assert!(options.scroll_into_view);
assert!(options.simulate_typing);
assert!(options.check_empty);
assert!(options.verify);
}
#[test]
fn fill_result_success() {
let result = FillResult::success("filled text".to_string());
assert!(result.filled);
assert!(result.verified);
assert!(!result.skipped);
assert_eq!(result.actual_value, Some("filled text".to_string()));
}
#[test]
fn fill_result_skipped() {
let result = FillResult::skipped("existing text".to_string());
assert!(!result.filled);
assert!(!result.verified);
assert!(result.skipped);
assert_eq!(result.actual_value, Some("existing text".to_string()));
}
#[test]
fn fill_result_failed() {
let result = FillResult::failed();
assert!(!result.filled);
assert!(!result.verified);
assert!(!result.skipped);
assert!(result.actual_value.is_none());
}
}