Skip to main content

browser_commander/elements/
content.rs

1//! Element content utilities.
2//!
3//! This module provides utilities for extracting content from DOM elements.
4
5use crate::core::engine::{EngineAdapter, EngineError};
6
7/// Get the text content of an element.
8///
9/// # Arguments
10///
11/// * `adapter` - The engine adapter to use
12/// * `selector` - The CSS selector for the element
13///
14/// # Returns
15///
16/// The text content of the element, or `None` if not found
17pub async fn text_content(
18    adapter: &dyn EngineAdapter,
19    selector: &str,
20) -> Result<Option<String>, EngineError> {
21    adapter.text_content(selector).await
22}
23
24/// Get the value of an input element.
25///
26/// # Arguments
27///
28/// * `adapter` - The engine adapter to use
29/// * `selector` - The CSS selector for the input element
30///
31/// # Returns
32///
33/// The input value, or `None` if not found
34pub async fn input_value(
35    adapter: &dyn EngineAdapter,
36    selector: &str,
37) -> Result<Option<String>, EngineError> {
38    adapter.input_value(selector).await
39}
40
41/// Get an attribute value from an element.
42///
43/// # Arguments
44///
45/// * `adapter` - The engine adapter to use
46/// * `selector` - The CSS selector for the element
47/// * `attribute` - The attribute name
48///
49/// # Returns
50///
51/// The attribute value, or `None` if not found
52pub async fn get_attribute(
53    adapter: &dyn EngineAdapter,
54    selector: &str,
55    attribute: &str,
56) -> Result<Option<String>, EngineError> {
57    adapter.get_attribute(selector, attribute).await
58}
59
60/// Check if an input element is empty.
61///
62/// # Arguments
63///
64/// * `adapter` - The engine adapter to use
65/// * `selector` - The CSS selector for the input element
66///
67/// # Returns
68///
69/// `true` if the element is empty or has only whitespace
70pub async fn is_element_empty(
71    adapter: &dyn EngineAdapter,
72    selector: &str,
73) -> Result<bool, EngineError> {
74    let value = adapter.input_value(selector).await?;
75    Ok(value.is_none_or(|v| v.trim().is_empty()))
76}
77
78/// Information about an element for logging purposes.
79#[derive(Debug, Clone)]
80pub struct ElementLogInfo {
81    /// The element's tag name.
82    pub tag_name: String,
83    /// The element's text content (truncated).
84    pub text_preview: String,
85    /// Key attributes for identification.
86    pub attributes: Vec<(String, String)>,
87}
88
89impl std::fmt::Display for ElementLogInfo {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        write!(f, "<{}", self.tag_name)?;
92
93        for (name, value) in &self.attributes {
94            if !value.is_empty() {
95                write!(f, " {}=\"{}\"", name, value)?;
96            }
97        }
98
99        write!(f, ">")?;
100
101        if !self.text_preview.is_empty() {
102            write!(f, "{}", self.text_preview)?;
103        }
104
105        write!(f, "</{}>", self.tag_name)
106    }
107}
108
109/// Truncate a string for preview purposes.
110///
111/// # Arguments
112///
113/// * `s` - The string to truncate
114/// * `max_len` - Maximum length
115///
116/// # Returns
117///
118/// The truncated string with "..." if it was truncated
119pub fn truncate_for_preview(s: &str, max_len: usize) -> String {
120    let trimmed = s.trim();
121    if trimmed.len() <= max_len {
122        trimmed.to_string()
123    } else {
124        format!("{}...", &trimmed[..max_len])
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn truncate_for_preview_short_string() {
134        assert_eq!(truncate_for_preview("hello", 10), "hello");
135    }
136
137    #[test]
138    fn truncate_for_preview_long_string() {
139        assert_eq!(truncate_for_preview("hello world", 5), "hello...");
140    }
141
142    #[test]
143    fn truncate_for_preview_trims_whitespace() {
144        assert_eq!(truncate_for_preview("  hello  ", 10), "hello");
145    }
146
147    #[test]
148    fn truncate_for_preview_exact_length() {
149        assert_eq!(truncate_for_preview("hello", 5), "hello");
150    }
151
152    #[test]
153    fn element_log_info_display() {
154        let info = ElementLogInfo {
155            tag_name: "button".to_string(),
156            text_preview: "Click me".to_string(),
157            attributes: vec![
158                ("id".to_string(), "submit-btn".to_string()),
159                ("class".to_string(), "primary".to_string()),
160            ],
161        };
162
163        let display = format!("{}", info);
164        assert!(display.contains("button"));
165        assert!(display.contains("submit-btn"));
166        assert!(display.contains("Click me"));
167    }
168
169    #[test]
170    fn element_log_info_display_no_text() {
171        let info = ElementLogInfo {
172            tag_name: "input".to_string(),
173            text_preview: String::new(),
174            attributes: vec![("type".to_string(), "text".to_string())],
175        };
176
177        let display = format!("{}", info);
178        assert!(display.contains("input"));
179        assert!(display.contains("type"));
180        assert!(display.contains("text"));
181    }
182
183    #[test]
184    fn element_log_info_display_empty_attribute() {
185        let info = ElementLogInfo {
186            tag_name: "div".to_string(),
187            text_preview: "content".to_string(),
188            attributes: vec![
189                ("id".to_string(), "test".to_string()),
190                ("class".to_string(), String::new()),
191            ],
192        };
193
194        let display = format!("{}", info);
195        assert!(display.contains("id=\"test\""));
196        // Empty class should not be displayed
197        assert!(!display.contains("class="));
198    }
199}