browser_use/tools/
get_clickable_elements.rs

1use crate::error::Result;
2use crate::tools::{Tool, ToolContext, ToolResult};
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6/// Parameters for the get_clickable_elements tool (no parameters needed)
7#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
8pub struct GetClickableElementsParams {}
9
10/// Tool for getting all clickable/interactive elements on the page
11#[derive(Default)]
12pub struct GetClickableElementsTool;
13
14impl Tool for GetClickableElementsTool {
15    type Params = GetClickableElementsParams;
16
17    fn name(&self) -> &str {
18        "get_clickable_elements"
19    }
20
21    fn execute_typed(
22        &self,
23        _params: GetClickableElementsParams,
24        context: &mut ToolContext,
25    ) -> Result<ToolResult> {
26        // Get or extract the DOM tree
27        let dom = context.get_dom()?;
28
29        // Get all interactive element indices
30        let indices = dom.interactive_indices();
31
32        if indices.is_empty() {
33            return Ok(ToolResult::success_with(serde_json::json!({
34                "elements": "",
35                "count": 0
36            })));
37        }
38
39        // Format clickable elements similar to TypeScript implementation:
40        // [0]<button>Submit</button>
41        // [1]<a>Click here</a>
42        // [2]<input>
43        let mut formatted_elements = Vec::new();
44
45        for &index in &indices {
46            if let Some(node) = dom.find_node_by_index(index) {
47                let tag_name = &node.tag_name;
48
49                // Get text content, truncate if too long
50                let text_content = if let Some(text) = &node.text_content {
51                    let trimmed = text.trim();
52                    if trimmed.len() > 100 {
53                        format!("{}...", &trimmed[..97])
54                    } else {
55                        trimmed.to_string()
56                    }
57                } else {
58                    String::new()
59                };
60
61                // Format: [index]<tag>text</tag>
62                let formatted = if text_content.is_empty() {
63                    format!("[{}]<{}>", index, tag_name)
64                } else {
65                    format!("[{}]<{}>{}</{}>", index, tag_name, text_content, tag_name)
66                };
67
68                formatted_elements.push(formatted);
69            }
70        }
71
72        let elements_string = formatted_elements.join("\n");
73        let count = indices.len();
74
75        Ok(ToolResult::success_with(serde_json::json!({
76            "elements": elements_string,
77            "count": count
78        })))
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use crate::dom::DomTree;
86    use crate::dom::ElementNode;
87
88    #[test]
89    fn test_get_clickable_elements_params() {
90        let params = GetClickableElementsParams {};
91        let json = serde_json::to_value(params).unwrap();
92        assert!(json.is_object());
93    }
94
95    #[test]
96    fn test_tool_name() {
97        let tool = GetClickableElementsTool;
98        assert_eq!(tool.name(), "get_clickable_elements");
99    }
100
101    // Note: Full integration tests would require a real browser session
102    // This is a basic structure test
103    #[test]
104    fn test_empty_dom_tree() {
105        // Create a minimal DOM tree with no interactive elements
106        let root = ElementNode::new("body");
107        let dom_tree = DomTree::new(root);
108
109        // We can't easily test execute_typed without a real BrowserSession
110        // but we can verify the structure is correct
111        assert_eq!(dom_tree.count_interactive(), 0);
112    }
113}