1use super::*;
9
10#[derive(Debug, Clone, serde::Serialize)]
14pub struct FillFormOutcome {
15 pub filled: usize,
17 pub total: usize,
19 pub fields: Vec<FillFieldResult>,
21}
22
23#[derive(Debug, Clone, serde::Serialize)]
25pub struct FillFieldResult {
26 pub target: String,
28 pub action: String,
30 pub label: Option<String>,
32 pub success: bool,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub error: Option<String>,
37}
38
39const FILL_FORM_MAX_FIELDS: usize = 16;
41
42impl BrowserSession {
43 pub async fn fill_form(&self, fields: &[(&str, &str)]) -> BrowserResult<FillFormOutcome> {
52 let total = fields.len();
53 if total > FILL_FORM_MAX_FIELDS {
54 return Err(format!(
55 "fill_form: max {} fields, got {total}",
56 FILL_FORM_MAX_FIELDS
57 )
58 .into());
59 }
60
61 let mut resolved: Vec<(String, ResolvedElement)> = Vec::with_capacity(total);
63 for (target, _value) in fields {
64 let element = self
65 .resolve_element(target)
66 .await
67 .map_err(|e| format!("fill_form: resolution failed for \"{target}\": {e}"))?;
68 resolved.push(((*target).to_string(), element));
69 }
70
71 let mut results = Vec::with_capacity(total);
73 let mut filled = 0usize;
74
75 for ((target, element), (_t, value)) in resolved.iter().zip(fields.iter()) {
76 let (action, success, error) = self.fill_single_field(element, value).await;
77 if success {
78 filled += 1;
79 }
80 results.push(FillFieldResult {
81 target: target.clone(),
82 action,
83 label: Some(element.label.clone()),
84 success,
85 error,
86 });
87 }
88
89 Ok(FillFormOutcome {
90 filled,
91 total,
92 fields: results,
93 })
94 }
95
96 async fn fill_single_field(
97 &self,
98 element: &ResolvedElement,
99 value: &str,
100 ) -> (String, bool, Option<String>) {
101 let role = element.role.as_deref().unwrap_or("").to_lowercase();
102 let input_type = element.input_type.as_deref().unwrap_or("").to_lowercase();
103
104 let Some(ref reference) = element.reference else {
105 return (
106 "none".to_string(),
107 false,
108 Some("element has no reference".to_string()),
109 );
110 };
111
112 if matches!(role.as_str(), "listbox" | "combobox") {
113 match self.select_option(reference, value).await {
114 Ok(_) => ("select".to_string(), true, None),
115 Err(e) => ("select".to_string(), false, Some(e.to_string())),
116 }
117 } else if role == "checkbox" || input_type == "checkbox" {
118 let should_check =
119 !value.is_empty() && value != "false" && value != "0" && value != "off";
120 let (action, result) = if should_check {
121 ("check", self.check(reference).await)
122 } else {
123 ("uncheck", self.uncheck(reference).await)
124 };
125 match result {
126 Ok(_) => (action.to_string(), true, None),
127 Err(e) => (action.to_string(), false, Some(e.to_string())),
128 }
129 } else if role == "radio" || input_type == "radio" {
130 match self.click(reference).await {
131 Ok(_) => ("click".to_string(), true, None),
132 Err(e) => ("click".to_string(), false, Some(e.to_string())),
133 }
134 } else {
135 match self.type_text(value, Some(reference)).await {
136 Ok(_) => ("type".to_string(), true, None),
137 Err(e) => ("type".to_string(), false, Some(e.to_string())),
138 }
139 }
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn fill_field_result_success_serializes_without_error() {
149 let result = FillFieldResult {
150 target: "username".to_string(),
151 action: "type".to_string(),
152 label: Some("Username".to_string()),
153 success: true,
154 error: None,
155 };
156 let json = serde_json::to_value(&result).unwrap();
157 assert_eq!(json["target"], "username");
158 assert_eq!(json["action"], "type");
159 assert_eq!(json["label"], "Username");
160 assert_eq!(json["success"], true);
161 assert!(json.get("error").is_none());
162 }
163
164 #[test]
165 fn fill_field_result_failure_includes_error() {
166 let result = FillFieldResult {
167 target: "missing-field".to_string(),
168 action: "none".to_string(),
169 label: None,
170 success: false,
171 error: Some("element has no reference".to_string()),
172 };
173 let json = serde_json::to_value(&result).unwrap();
174 assert_eq!(json["success"], false);
175 assert_eq!(json["error"], "element has no reference");
176 assert!(json["label"].is_null());
177 }
178
179 #[test]
180 fn fill_form_outcome_counts_filled_and_total() {
181 let outcome = FillFormOutcome {
182 filled: 2,
183 total: 3,
184 fields: vec![
185 FillFieldResult {
186 target: "a".to_string(),
187 action: "type".to_string(),
188 label: None,
189 success: true,
190 error: None,
191 },
192 FillFieldResult {
193 target: "b".to_string(),
194 action: "check".to_string(),
195 label: None,
196 success: true,
197 error: None,
198 },
199 FillFieldResult {
200 target: "c".to_string(),
201 action: "none".to_string(),
202 label: None,
203 success: false,
204 error: Some("not found".to_string()),
205 },
206 ],
207 };
208 let json = serde_json::to_value(&outcome).unwrap();
209 assert_eq!(json["filled"], 2);
210 assert_eq!(json["total"], 3);
211 assert_eq!(json["fields"].as_array().unwrap().len(), 3);
212 }
213
214 #[test]
215 fn fill_form_max_fields_is_16() {
216 assert_eq!(FILL_FORM_MAX_FIELDS, 16);
217 }
218
219 #[test]
220 fn fill_form_max_fields_is_reasonable() {
221 const { assert!(FILL_FORM_MAX_FIELDS >= 1) };
223 const { assert!(FILL_FORM_MAX_FIELDS <= 64) };
225 }
226
227 #[test]
228 fn fill_form_rejects_exactly_one_over_max() {
229 const { assert!(FILL_FORM_MAX_FIELDS + 1 > FILL_FORM_MAX_FIELDS) };
231 const { assert!(FILL_FORM_MAX_FIELDS == 16) };
233 let err_msg = format!(
235 "fill_form: max {} fields, got {}",
236 FILL_FORM_MAX_FIELDS,
237 FILL_FORM_MAX_FIELDS + 1
238 );
239 assert!(err_msg.contains("max 16 fields"));
240 assert!(err_msg.contains("got 17"));
241 }
242
243 #[test]
244 fn fill_field_result_roundtrip_through_json() {
245 let result = FillFieldResult {
246 target: "email".to_string(),
247 action: "type".to_string(),
248 label: Some("Email Address".to_string()),
249 success: true,
250 error: None,
251 };
252 let json = serde_json::to_string(&result).unwrap();
253 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
255 assert_eq!(parsed["target"], "email");
256 assert_eq!(parsed["success"], true);
257 assert!(parsed.get("error").is_none());
258 }
259}