browser_commander/elements/
content.rs1use crate::core::engine::{EngineAdapter, EngineError};
6
7pub async fn text_content(
18 adapter: &dyn EngineAdapter,
19 selector: &str,
20) -> Result<Option<String>, EngineError> {
21 adapter.text_content(selector).await
22}
23
24pub async fn input_value(
35 adapter: &dyn EngineAdapter,
36 selector: &str,
37) -> Result<Option<String>, EngineError> {
38 adapter.input_value(selector).await
39}
40
41pub 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
60pub 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#[derive(Debug, Clone)]
80pub struct ElementLogInfo {
81 pub tag_name: String,
83 pub text_preview: String,
85 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
109pub 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 assert!(!display.contains("class="));
198 }
199}