1use super::*;
9
10#[derive(Debug, Clone, serde::Serialize)]
14pub struct FillFormOutcome {
15 pub status: ActionStatus,
16 #[serde(rename = "failureKind", skip_serializing_if = "Option::is_none")]
17 pub failure_kind: Option<ActionFailureKind>,
18 #[serde(rename = "executionId")]
19 pub execution_id: String,
20 pub filled: usize,
22 pub total: usize,
24 pub fields: Vec<FillFieldResult>,
26 #[serde(rename = "previousRevision")]
27 pub previous_revision: u64,
28 #[serde(rename = "currentRevision")]
29 pub current_revision: u64,
30 pub verification: ActionVerificationEvidence,
31}
32
33#[derive(Debug, Clone, serde::Serialize)]
35pub struct FillFieldResult {
36 pub target: String,
38 pub action: String,
40 pub label: Option<String>,
42 pub success: bool,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub error: Option<String>,
47}
48
49const FILL_FORM_MAX_FIELDS: usize = 16;
51
52impl BrowserSession {
53 pub async fn fill_form(&self, fields: &[(&str, &str)]) -> BrowserResult<FillFormOutcome> {
62 self.fill_form_with_expected_revision(fields, None).await
63 }
64
65 pub async fn fill_form_with_expected_revision(
67 &self,
68 fields: &[(&str, &str)],
69 expected_revision: Option<u64>,
70 ) -> BrowserResult<FillFormOutcome> {
71 self.require_expected_revision(expected_revision)?;
72 let previous_revision = self
73 .page_revision
74 .load(std::sync::atomic::Ordering::Relaxed);
75 let before = self.page_info().await.ok();
76 let total = fields.len();
77 if total > FILL_FORM_MAX_FIELDS {
78 return Err(format!(
79 "fill_form: max {} fields, got {total}",
80 FILL_FORM_MAX_FIELDS
81 )
82 .into());
83 }
84
85 let mut resolved: Vec<(String, ResolvedElement)> = Vec::with_capacity(total);
87 for (target, _value) in fields {
88 let element = self.resolve_element(target).await?;
89 resolved.push(((*target).to_string(), element));
90 }
91
92 let mut results = Vec::with_capacity(total);
94 let mut filled = 0usize;
95
96 for ((target, element), (_t, value)) in resolved.iter().zip(fields.iter()) {
97 let (action, success, error) = self.fill_single_field(element, value).await;
98 if success {
99 filled += 1;
100 }
101 results.push(FillFieldResult {
102 target: target.clone(),
103 action,
104 label: Some(element.label.clone()),
105 success,
106 error,
107 });
108 }
109
110 let current_revision = self
111 .page_revision
112 .load(std::sync::atomic::Ordering::Relaxed);
113 let after = self.page_info().await.ok();
114 Ok(FillFormOutcome {
115 status: if filled == total {
116 ActionStatus::Succeeded
117 } else {
118 ActionStatus::CompletedWithVerificationFailure
119 },
120 failure_kind: (filled != total).then_some(ActionFailureKind::VerificationFailed),
121 execution_id: self.next_execution_id(),
122 filled,
123 total,
124 fields: results,
125 previous_revision,
126 current_revision,
127 verification: ActionVerificationEvidence {
128 revision_delta: current_revision.saturating_sub(previous_revision),
129 url_changed: before
130 .as_ref()
131 .zip(after.as_ref())
132 .is_some_and(|(before, after)| before.url != after.url),
133 title_changed: before
134 .as_ref()
135 .zip(after.as_ref())
136 .is_some_and(|(before, after)| before.title != after.title),
137 ..ActionVerificationEvidence::default()
138 },
139 })
140 }
141
142 async fn fill_single_field(
143 &self,
144 element: &ResolvedElement,
145 value: &str,
146 ) -> (String, bool, Option<String>) {
147 let role = element.role.as_deref().unwrap_or("").to_lowercase();
148 let input_type = element.input_type.as_deref().unwrap_or("").to_lowercase();
149
150 let Some(ref reference) = element.reference else {
151 return (
152 "none".to_string(),
153 false,
154 Some("element has no reference".to_string()),
155 );
156 };
157
158 if matches!(role.as_str(), "listbox" | "combobox") {
159 match self.select_option(reference, value).await {
160 Ok(_) => ("select".to_string(), true, None),
161 Err(e) => ("select".to_string(), false, Some(e.to_string())),
162 }
163 } else if role == "checkbox" || input_type == "checkbox" {
164 let should_check =
165 !value.is_empty() && value != "false" && value != "0" && value != "off";
166 let (action, result) = if should_check {
167 ("check", self.check(reference).await)
168 } else {
169 ("uncheck", self.uncheck(reference).await)
170 };
171 match result {
172 Ok(_) => (action.to_string(), true, None),
173 Err(e) => (action.to_string(), false, Some(e.to_string())),
174 }
175 } else if role == "radio" || input_type == "radio" {
176 match self.click(reference).await {
177 Ok(_) => ("click".to_string(), true, None),
178 Err(e) => ("click".to_string(), false, Some(e.to_string())),
179 }
180 } else {
181 match self.type_text(value, Some(reference)).await {
182 Ok(_) => ("type".to_string(), true, None),
183 Err(e) => ("type".to_string(), false, Some(e.to_string())),
184 }
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn fill_field_result_success_serializes_without_error() {
195 let result = FillFieldResult {
196 target: "username".to_string(),
197 action: "type".to_string(),
198 label: Some("Username".to_string()),
199 success: true,
200 error: None,
201 };
202 let json = serde_json::to_value(&result).unwrap();
203 assert_eq!(json["target"], "username");
204 assert_eq!(json["action"], "type");
205 assert_eq!(json["label"], "Username");
206 assert_eq!(json["success"], true);
207 assert!(json.get("error").is_none());
208 }
209
210 #[test]
211 fn fill_field_result_failure_includes_error() {
212 let result = FillFieldResult {
213 target: "missing-field".to_string(),
214 action: "none".to_string(),
215 label: None,
216 success: false,
217 error: Some("element has no reference".to_string()),
218 };
219 let json = serde_json::to_value(&result).unwrap();
220 assert_eq!(json["success"], false);
221 assert_eq!(json["error"], "element has no reference");
222 assert!(json["label"].is_null());
223 }
224
225 #[test]
226 fn fill_form_outcome_counts_filled_and_total() {
227 let outcome = FillFormOutcome {
228 status: ActionStatus::CompletedWithVerificationFailure,
229 failure_kind: Some(ActionFailureKind::VerificationFailed),
230 execution_id: "act_test_1".to_string(),
231 filled: 2,
232 total: 3,
233 fields: vec![
234 FillFieldResult {
235 target: "a".to_string(),
236 action: "type".to_string(),
237 label: None,
238 success: true,
239 error: None,
240 },
241 FillFieldResult {
242 target: "b".to_string(),
243 action: "check".to_string(),
244 label: None,
245 success: true,
246 error: None,
247 },
248 FillFieldResult {
249 target: "c".to_string(),
250 action: "none".to_string(),
251 label: None,
252 success: false,
253 error: Some("not found".to_string()),
254 },
255 ],
256 previous_revision: 1,
257 current_revision: 3,
258 verification: ActionVerificationEvidence {
259 revision_delta: 2,
260 ..ActionVerificationEvidence::default()
261 },
262 };
263 let json = serde_json::to_value(&outcome).unwrap();
264 assert_eq!(json["filled"], 2);
265 assert_eq!(json["total"], 3);
266 assert_eq!(json["fields"].as_array().unwrap().len(), 3);
267 }
268
269 #[test]
270 fn fill_form_max_fields_is_16() {
271 assert_eq!(FILL_FORM_MAX_FIELDS, 16);
272 }
273
274 #[test]
275 fn fill_form_max_fields_is_reasonable() {
276 const { assert!(FILL_FORM_MAX_FIELDS >= 1) };
278 const { assert!(FILL_FORM_MAX_FIELDS <= 64) };
280 }
281
282 #[test]
283 fn fill_form_rejects_exactly_one_over_max() {
284 const { assert!(FILL_FORM_MAX_FIELDS + 1 > FILL_FORM_MAX_FIELDS) };
286 const { assert!(FILL_FORM_MAX_FIELDS == 16) };
288 let err_msg = format!(
290 "fill_form: max {} fields, got {}",
291 FILL_FORM_MAX_FIELDS,
292 FILL_FORM_MAX_FIELDS + 1
293 );
294 assert!(err_msg.contains("max 16 fields"));
295 assert!(err_msg.contains("got 17"));
296 }
297
298 #[test]
299 fn fill_field_result_roundtrip_through_json() {
300 let result = FillFieldResult {
301 target: "email".to_string(),
302 action: "type".to_string(),
303 label: Some("Email Address".to_string()),
304 success: true,
305 error: None,
306 };
307 let json = serde_json::to_string(&result).unwrap();
308 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
310 assert_eq!(parsed["target"], "email");
311 assert_eq!(parsed["success"], true);
312 assert!(parsed.get("error").is_none());
313 }
314}