viewpoint_core/page/locator/queries/
text.rs

1//! Text query methods for locators.
2
3use viewpoint_cdp::protocol::dom::BackendNodeId;
4use viewpoint_js::js;
5
6use super::super::Locator;
7use super::super::Selector;
8use crate::error::LocatorError;
9
10impl Locator<'_> {
11    /// Get the inner text of all matching elements.
12    ///
13    /// Returns the `innerText` property for each element, which is the rendered
14    /// text content as it appears on screen (respects CSS styling like `display: none`).
15    ///
16    /// # Example
17    ///
18    /// ```no_run
19    /// use viewpoint_core::Page;
20    ///
21    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
22    /// let texts = page.locator("li").all_inner_texts().await?;
23    /// assert_eq!(texts, vec!["Item 1", "Item 2", "Item 3"]);
24    /// # Ok(())
25    /// # }
26    /// ```
27    ///
28    /// # Errors
29    ///
30    /// Returns an error if the elements cannot be queried.
31    pub async fn all_inner_texts(&self) -> Result<Vec<String>, LocatorError> {
32        // Handle Ref selector - returns single element's inner text as array
33        if let Selector::Ref(ref_str) = &self.selector {
34            let backend_node_id = self.page.get_backend_node_id_for_ref(ref_str)?;
35            return self.all_inner_texts_by_backend_id(backend_node_id).await;
36        }
37
38        // Handle BackendNodeId selector
39        if let Selector::BackendNodeId(backend_node_id) = &self.selector {
40            return self.all_inner_texts_by_backend_id(*backend_node_id).await;
41        }
42
43        let selector_expr = self.selector.to_js_expression();
44        let js_code = js! {
45            (function() {
46                const elements = Array.from(@{selector_expr});
47                return elements.map(el => el.innerText || "");
48            })()
49        };
50
51        let result = self.evaluate_js(&js_code).await?;
52
53        result
54            .as_array()
55            .map(|arr| {
56                arr.iter()
57                    .map(|v| v.as_str().unwrap_or("").to_string())
58                    .collect()
59            })
60            .ok_or_else(|| LocatorError::EvaluationError("Expected array result".to_string()))
61    }
62
63    /// Get all inner texts by backend node ID (returns single element as array).
64    pub(super) async fn all_inner_texts_by_backend_id(
65        &self,
66        backend_node_id: BackendNodeId,
67    ) -> Result<Vec<String>, LocatorError> {
68        let js_fn = js! {
69            (function() {
70                return [this.innerText || ""];
71            })
72        };
73        // Strip outer parentheses for CDP functionDeclaration
74        let js_fn = js_fn.trim_start_matches('(').trim_end_matches(')');
75
76        let result = self
77            .call_function_on_backend_id(backend_node_id, js_fn)
78            .await?;
79
80        result
81            .as_array()
82            .map(|arr| {
83                arr.iter()
84                    .map(|v| v.as_str().unwrap_or("").to_string())
85                    .collect()
86            })
87            .ok_or_else(|| LocatorError::EvaluationError("Expected array result".to_string()))
88    }
89
90    /// Get the text content of all matching elements.
91    ///
92    /// Returns the `textContent` property for each element, which includes all
93    /// text including hidden elements.
94    ///
95    /// # Example
96    ///
97    /// ```no_run
98    /// use viewpoint_core::Page;
99    ///
100    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
101    /// let texts = page.locator("li").all_text_contents().await?;
102    /// assert_eq!(texts, vec!["Item 1", "Item 2", "Item 3"]);
103    /// # Ok(())
104    /// # }
105    /// ```
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if the elements cannot be queried.
110    pub async fn all_text_contents(&self) -> Result<Vec<String>, LocatorError> {
111        // Handle Ref selector - returns single element's text content as array
112        if let Selector::Ref(ref_str) = &self.selector {
113            let backend_node_id = self.page.get_backend_node_id_for_ref(ref_str)?;
114            return self.all_text_contents_by_backend_id(backend_node_id).await;
115        }
116
117        // Handle BackendNodeId selector
118        if let Selector::BackendNodeId(backend_node_id) = &self.selector {
119            return self.all_text_contents_by_backend_id(*backend_node_id).await;
120        }
121
122        let selector_expr = self.selector.to_js_expression();
123        let js_code = js! {
124            (function() {
125                const elements = Array.from(@{selector_expr});
126                return elements.map(el => el.textContent || "");
127            })()
128        };
129
130        let result = self.evaluate_js(&js_code).await?;
131
132        result
133            .as_array()
134            .map(|arr| {
135                arr.iter()
136                    .map(|v| v.as_str().unwrap_or("").to_string())
137                    .collect()
138            })
139            .ok_or_else(|| LocatorError::EvaluationError("Expected array result".to_string()))
140    }
141
142    /// Get all text contents by backend node ID (returns single element as array).
143    pub(super) async fn all_text_contents_by_backend_id(
144        &self,
145        backend_node_id: BackendNodeId,
146    ) -> Result<Vec<String>, LocatorError> {
147        let js_fn = js! {
148            (function() {
149                return [this.textContent || ""];
150            })
151        };
152        // Strip outer parentheses for CDP functionDeclaration
153        let js_fn = js_fn.trim_start_matches('(').trim_end_matches(')');
154
155        let result = self
156            .call_function_on_backend_id(backend_node_id, js_fn)
157            .await?;
158
159        result
160            .as_array()
161            .map(|arr| {
162                arr.iter()
163                    .map(|v| v.as_str().unwrap_or("").to_string())
164                    .collect()
165            })
166            .ok_or_else(|| LocatorError::EvaluationError("Expected array result".to_string()))
167    }
168
169    /// Get the inner text of the first matching element.
170    ///
171    /// Returns the `innerText` property, which is the rendered text content.
172    ///
173    /// # Errors
174    ///
175    /// Returns an error if the element cannot be queried.
176    pub async fn inner_text(&self) -> Result<String, LocatorError> {
177        // Handle Ref selector - lookup in ref map and resolve via CDP
178        if let Selector::Ref(ref_str) = &self.selector {
179            let backend_node_id = self.page.get_backend_node_id_for_ref(ref_str)?;
180            return self.inner_text_by_backend_id(backend_node_id).await;
181        }
182
183        // Handle BackendNodeId selector
184        if let Selector::BackendNodeId(backend_node_id) = &self.selector {
185            return self.inner_text_by_backend_id(*backend_node_id).await;
186        }
187
188        let selector_expr = self.selector.to_js_expression();
189        let js_code = js! {
190            (function() {
191                const elements = @{selector_expr};
192                if (elements.length === 0) return { found: false };
193                return { found: true, text: elements[0].innerText || "" };
194            })()
195        };
196
197        let result = self.evaluate_js(&js_code).await?;
198
199        let found = result
200            .get("found")
201            .and_then(serde_json::Value::as_bool)
202            .unwrap_or(false);
203        if !found {
204            return Err(LocatorError::NotFound(format!("{:?}", self.selector)));
205        }
206
207        Ok(result
208            .get("text")
209            .and_then(|v| v.as_str())
210            .unwrap_or("")
211            .to_string())
212    }
213
214    /// Get inner text by backend node ID.
215    pub(super) async fn inner_text_by_backend_id(
216        &self,
217        backend_node_id: BackendNodeId,
218    ) -> Result<String, LocatorError> {
219        let js_fn = js! {
220            (function() {
221                return { text: this.innerText || "" };
222            })
223        };
224        // Strip outer parentheses for CDP functionDeclaration
225        let js_fn = js_fn.trim_start_matches('(').trim_end_matches(')');
226
227        let result = self
228            .call_function_on_backend_id(backend_node_id, js_fn)
229            .await?;
230
231        Ok(result
232            .get("text")
233            .and_then(|v| v.as_str())
234            .unwrap_or("")
235            .to_string())
236    }
237}