browser_use/dom/
element.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// Represents a DOM element node
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
6pub struct ElementNode {
7    /// HTML tag name (e.g., "div", "button", "input")
8    pub tag_name: String,
9
10    /// Element attributes (e.g., id, class, href, etc.)
11    #[serde(default)]
12    pub attributes: HashMap<String, String>,
13
14    /// Text content of the element
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub text_content: Option<String>,
17
18    /// Child elements
19    #[serde(default, skip_serializing_if = "Vec::is_empty")]
20    pub children: Vec<ElementNode>,
21
22    /// Index assigned to this element (for interactive elements)
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub index: Option<usize>,
25
26    /// Whether the element is visible in the viewport
27    #[serde(default)]
28    pub is_visible: bool,
29
30    /// Whether the element is interactive (clickable, input, etc.)
31    #[serde(default)]
32    pub is_interactive: bool,
33
34    /// Bounding box information (x, y, width, height)
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub bounding_box: Option<BoundingBox>,
37}
38
39/// Bounding box coordinates for an element
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
41pub struct BoundingBox {
42    pub x: f64,
43    pub y: f64,
44    pub width: f64,
45    pub height: f64,
46}
47
48impl ElementNode {
49    /// Create a new ElementNode
50    pub fn new(tag_name: impl Into<String>) -> Self {
51        Self {
52            tag_name: tag_name.into(),
53            attributes: HashMap::new(),
54            text_content: None,
55            children: Vec::new(),
56            index: None,
57            is_visible: false,
58            is_interactive: false,
59            bounding_box: None,
60        }
61    }
62
63    /// Builder method: set attributes
64    pub fn with_attributes(mut self, attributes: HashMap<String, String>) -> Self {
65        self.attributes = attributes;
66        self
67    }
68
69    /// Builder method: set text content
70    pub fn with_text(mut self, text: impl Into<String>) -> Self {
71        self.text_content = Some(text.into());
72        self
73    }
74
75    /// Builder method: set children
76    pub fn with_children(mut self, children: Vec<ElementNode>) -> Self {
77        self.children = children;
78        self
79    }
80
81    /// Builder method: set index
82    pub fn with_index(mut self, index: usize) -> Self {
83        self.index = Some(index);
84        self
85    }
86
87    /// Builder method: set visibility
88    pub fn with_visibility(mut self, visible: bool) -> Self {
89        self.is_visible = visible;
90        self
91    }
92
93    /// Builder method: set interactivity
94    pub fn with_interactivity(mut self, interactive: bool) -> Self {
95        self.is_interactive = interactive;
96        self
97    }
98
99    /// Builder method: set bounding box
100    pub fn with_bounding_box(mut self, x: f64, y: f64, width: f64, height: f64) -> Self {
101        self.bounding_box = Some(BoundingBox {
102            x,
103            y,
104            width,
105            height,
106        });
107        self
108    }
109
110    /// Add a single attribute
111    pub fn add_attribute(&mut self, key: impl Into<String>, value: impl Into<String>) {
112        self.attributes.insert(key.into(), value.into());
113    }
114
115    /// Add a child element
116    pub fn add_child(&mut self, child: ElementNode) {
117        self.children.push(child);
118    }
119
120    /// Get attribute value by key
121    pub fn get_attribute(&self, key: &str) -> Option<&String> {
122        self.attributes.get(key)
123    }
124
125    /// Check if element has a specific class
126    pub fn has_class(&self, class_name: &str) -> bool {
127        if let Some(classes) = self.attributes.get("class") {
128            classes.split_whitespace().any(|c| c == class_name)
129        } else {
130            false
131        }
132    }
133
134    /// Get element ID
135    pub fn id(&self) -> Option<&String> {
136        self.attributes.get("id")
137    }
138
139    /// Check if element is a specific tag
140    pub fn is_tag(&self, tag: &str) -> bool {
141        self.tag_name.eq_ignore_ascii_case(tag)
142    }
143
144    /// Determine if this element should be considered interactive
145    pub fn compute_interactivity(&mut self) {
146        // Interactive tags
147        let interactive_tags = ["button", "a", "input", "select", "textarea", "label"];
148
149        // Check if tag is interactive
150        let tag_is_interactive = interactive_tags.iter().any(|&tag| self.is_tag(tag));
151
152        // Check for onclick or other event handlers
153        let has_event_handler = self.attributes.keys().any(|k| {
154            k.starts_with("on")
155                || k == "role" && self.attributes.get("role").map_or(false, |r| r == "button")
156        });
157
158        // Check for clickable role
159        let has_clickable_role = self.get_attribute("role").map_or(false, |r| {
160            ["button", "link", "tab", "menuitem"].contains(&r.as_str())
161        });
162
163        self.is_interactive = tag_is_interactive || has_event_handler || has_clickable_role;
164    }
165
166    /// Simplify element by removing unnecessary children (like scripts, styles)
167    pub fn simplify(&mut self) {
168        // Remove script, style, and noscript elements
169        self.children
170            .retain(|child| !matches!(child.tag_name.as_str(), "script" | "style" | "noscript"));
171
172        // Recursively simplify children
173        for child in &mut self.children {
174            child.simplify();
175        }
176    }
177
178    /// Convert to a simplified string representation
179    pub fn to_simple_string(&self) -> String {
180        let mut parts = vec![format!("<{}", self.tag_name)];
181
182        if let Some(id) = self.id() {
183            parts.push(format!(" id=\"{}\"", id));
184        }
185
186        if let Some(class) = self.attributes.get("class") {
187            parts.push(format!(" class=\"{}\"", class));
188        }
189
190        if let Some(index) = self.index {
191            parts.push(format!(" data-index=\"{}\"", index));
192        }
193
194        parts.push(">".to_string());
195
196        if let Some(text) = &self.text_content {
197            if !text.trim().is_empty() {
198                parts.push(text.trim().to_string());
199            }
200        }
201
202        parts.join("")
203    }
204}
205
206impl BoundingBox {
207    /// Create a new BoundingBox
208    pub fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
209        Self {
210            x,
211            y,
212            width,
213            height,
214        }
215    }
216
217    /// Check if the bounding box is visible (has non-zero dimensions)
218    pub fn is_visible(&self) -> bool {
219        self.width > 0.0 && self.height > 0.0
220    }
221
222    /// Calculate the area of the bounding box
223    pub fn area(&self) -> f64 {
224        self.width * self.height
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_element_node_creation() {
234        let mut attrs = HashMap::new();
235        attrs.insert("id".to_string(), "test-id".to_string());
236        attrs.insert("class".to_string(), "btn primary".to_string());
237
238        let element = ElementNode::new("button")
239            .with_attributes(attrs)
240            .with_text("Click me")
241            .with_index(1)
242            .with_visibility(true)
243            .with_interactivity(true);
244
245        assert_eq!(element.tag_name, "button");
246        assert_eq!(element.id(), Some(&"test-id".to_string()));
247        assert_eq!(element.text_content, Some("Click me".to_string()));
248        assert_eq!(element.index, Some(1));
249        assert!(element.is_visible);
250        assert!(element.is_interactive);
251    }
252
253    #[test]
254    fn test_has_class() {
255        let mut element = ElementNode::new("div");
256        element.add_attribute("class", "container main active");
257
258        assert!(element.has_class("container"));
259        assert!(element.has_class("main"));
260        assert!(element.has_class("active"));
261        assert!(!element.has_class("hidden"));
262    }
263
264    #[test]
265    fn test_compute_interactivity() {
266        let mut button = ElementNode::new("button");
267        button.compute_interactivity();
268        assert!(button.is_interactive);
269
270        let mut div = ElementNode::new("div");
271        div.compute_interactivity();
272        assert!(!div.is_interactive);
273
274        let mut clickable_div = ElementNode::new("div");
275        clickable_div.add_attribute("onclick", "alert('hi')");
276        clickable_div.compute_interactivity();
277        assert!(clickable_div.is_interactive);
278
279        let mut role_button = ElementNode::new("div");
280        role_button.add_attribute("role", "button");
281        role_button.compute_interactivity();
282        assert!(role_button.is_interactive);
283    }
284
285    #[test]
286    fn test_simplify() {
287        let mut parent = ElementNode::new("div");
288        parent.add_child(ElementNode::new("p").with_text("Content"));
289        parent.add_child(ElementNode::new("script").with_text("alert('test')"));
290        parent.add_child(ElementNode::new("style").with_text(".test { color: red; }"));
291        parent.add_child(ElementNode::new("span").with_text("More content"));
292
293        parent.simplify();
294
295        assert_eq!(parent.children.len(), 2);
296        assert!(parent.children[0].is_tag("p"));
297        assert!(parent.children[1].is_tag("span"));
298    }
299
300    #[test]
301    fn test_serialization() {
302        let element = ElementNode::new("button")
303            .with_text("Click")
304            .with_index(5)
305            .with_visibility(true);
306
307        let json = serde_json::to_string(&element).unwrap();
308        let deserialized: ElementNode = serde_json::from_str(&json).unwrap();
309
310        assert_eq!(element, deserialized);
311    }
312
313    #[test]
314    fn test_bounding_box() {
315        let bbox = BoundingBox::new(10.0, 20.0, 100.0, 50.0);
316
317        assert!(bbox.is_visible());
318        assert_eq!(bbox.area(), 5000.0);
319
320        let invisible_bbox = BoundingBox::new(0.0, 0.0, 0.0, 0.0);
321        assert!(!invisible_bbox.is_visible());
322    }
323
324    #[test]
325    fn test_to_simple_string() {
326        let mut attrs = HashMap::new();
327        attrs.insert("id".to_string(), "my-btn".to_string());
328        attrs.insert("class".to_string(), "btn primary".to_string());
329
330        let element = ElementNode::new("button")
331            .with_attributes(attrs)
332            .with_text("Submit")
333            .with_index(10);
334
335        let simple = element.to_simple_string();
336        assert!(simple.contains("<button"));
337        assert!(simple.contains("id=\"my-btn\""));
338        assert!(simple.contains("class=\"btn primary\""));
339        assert!(simple.contains("data-index=\"10\""));
340        assert!(simple.contains("Submit"));
341    }
342}