Skip to main content

glass/browser/session/
fill.rs

1//! High-level form filling.
2//!
3//! Provides atomically-resolved multi-field form fill via
4//! [`BrowserSession::fill_form`]. Each field is resolved, then
5//! the appropriate action (type, check, uncheck, select, click) is
6//! applied based on the element's accessibility role.
7
8use super::*;
9
10/// Outcome of a high-level form fill operation.
11///
12/// Returned by [`BrowserSession::fill_form`].
13#[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    /// Number of fields successfully filled.
21    pub filled: usize,
22    /// Total number of fields submitted.
23    pub total: usize,
24    /// Per-field result with action taken and status.
25    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/// Result for a single field within a form fill operation.
34#[derive(Debug, Clone, serde::Serialize)]
35pub struct FillFieldResult {
36    /// Locator target that was resolved.
37    pub target: String,
38    /// Action applied: `"type"`, `"select"`, `"check"`, `"uncheck"`, `"click"`.
39    pub action: String,
40    /// Accessible label of the element, if available.
41    pub label: Option<String>,
42    /// Whether the action succeeded.
43    pub success: bool,
44    /// Error message if the action failed.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub error: Option<String>,
47}
48
49/// Maximum fields accepted in a single [`BrowserSession::fill_form`] call.
50const FILL_FORM_MAX_FIELDS: usize = 16;
51
52impl BrowserSession {
53    /// Fill multiple form fields atomically.
54    ///
55    /// First resolves all locators (failing atomically on any resolution
56    /// error), then applies the appropriate action per field based on
57    /// its role: text inputs get typed, checkboxes/radios get
58    /// checked/unchecked, selects/combo boxes get option selection.
59    ///
60    /// Bounded to 16 fields per call.
61    pub async fn fill_form(&self, fields: &[(&str, &str)]) -> BrowserResult<FillFormOutcome> {
62        self.fill_form_with_expected_revision(fields, None).await
63    }
64
65    /// Fill a form while optionally enforcing the caller's observation revision.
66    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        // Phase 1: resolve all locators atomically
86        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        // Phase 2: apply actions
93        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        // Must be at least 1 to allow single-field fills
277        const { assert!(FILL_FORM_MAX_FIELDS >= 1) };
278        // Must be at most 64 to prevent unbounded allocation
279        const { assert!(FILL_FORM_MAX_FIELDS <= 64) };
280    }
281
282    #[test]
283    fn fill_form_rejects_exactly_one_over_max() {
284        // Verify the bound check: exactly 17 fields exceeds 16-field cap
285        const { assert!(FILL_FORM_MAX_FIELDS + 1 > FILL_FORM_MAX_FIELDS) };
286        // Boundary: exactly at max should pass the bound check
287        const { assert!(FILL_FORM_MAX_FIELDS == 16) };
288        // The error message uses the constant
289        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        // We can't deserialize back (no Deserialize impl), but verify JSON structure
309        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}