browser_commander/interactions/
fill.rs1use 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#[derive(Debug, Clone)]
16pub struct FillOptions {
17 pub scroll_into_view: bool,
19 pub simulate_typing: bool,
21 pub check_empty: bool,
23 pub verify: bool,
25 pub verification_timeout: Duration,
27 pub verification_retry_interval: Duration,
29 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#[derive(Debug, Clone)]
49pub struct FillResult {
50 pub filled: bool,
52 pub verified: bool,
54 pub skipped: bool,
56 pub actual_value: Option<String>,
58}
59
60impl FillResult {
61 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 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 pub fn failed() -> Self {
83 Self {
84 filled: false,
85 verified: false,
86 skipped: false,
87 actual_value: None,
88 }
89 }
90}
91
92pub 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 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 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
153pub async fn perform_fill(
166 adapter: &dyn EngineAdapter,
167 selector: &str,
168 text: &str,
169 options: &FillOptions,
170) -> Result<FillResult, EngineError> {
171 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 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
209pub async fn fill_text_area(
229 adapter: &dyn EngineAdapter,
230 selector: &str,
231 text: &str,
232 options: &FillOptions,
233) -> Result<FillResult, EngineError> {
234 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 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 let click_options = crate::interactions::click::ClickOptions {
257 scroll_into_view: false, verify: false, ..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_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}