viewpoint_core/page/locator/queries/
attributes.rs

1//! Attribute and value 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 an attribute value from the first matching element.
12    ///
13    /// # Errors
14    ///
15    /// Returns an error if the element cannot be queried.
16    pub async fn get_attribute(&self, name: &str) -> Result<Option<String>, LocatorError> {
17        // Handle Ref selector - lookup in ref map and resolve via CDP
18        if let Selector::Ref(ref_str) = &self.selector {
19            let backend_node_id = self.page.get_backend_node_id_for_ref(ref_str)?;
20            return self
21                .get_attribute_by_backend_id(backend_node_id, name)
22                .await;
23        }
24
25        // Handle BackendNodeId selector
26        if let Selector::BackendNodeId(backend_node_id) = &self.selector {
27            return self
28                .get_attribute_by_backend_id(*backend_node_id, name)
29                .await;
30        }
31
32        let selector_expr = self.selector.to_js_expression();
33        let js_code = js! {
34            (function() {
35                const elements = @{selector_expr};
36                if (elements.length === 0) return { found: false };
37                const attr = elements[0].getAttribute(#{name});
38                return { found: true, value: attr };
39            })()
40        };
41
42        let result = self.evaluate_js(&js_code).await?;
43
44        let found = result
45            .get("found")
46            .and_then(serde_json::Value::as_bool)
47            .unwrap_or(false);
48        if !found {
49            return Err(LocatorError::NotFound(format!("{:?}", self.selector)));
50        }
51
52        Ok(result
53            .get("value")
54            .and_then(|v| if v.is_null() { None } else { v.as_str() })
55            .map(std::string::ToString::to_string))
56    }
57
58    /// Get attribute by backend node ID.
59    pub(super) async fn get_attribute_by_backend_id(
60        &self,
61        backend_node_id: BackendNodeId,
62        name: &str,
63    ) -> Result<Option<String>, LocatorError> {
64        // Build function declaration for CDP callFunctionOn
65        // Wrapping in parens makes it a valid expression for js! macro parsing
66        let js_fn = js! {
67            (function() {
68                const attr = this.getAttribute(#{name});
69                return { value: attr };
70            })
71        };
72        // Strip outer parentheses for CDP (it expects function declaration syntax)
73        let js_fn = js_fn.trim_start_matches('(').trim_end_matches(')');
74        let result = self
75            .call_function_on_backend_id_with_fn(backend_node_id, js_fn)
76            .await?;
77
78        Ok(result
79            .get("value")
80            .and_then(|v| if v.is_null() { None } else { v.as_str() })
81            .map(std::string::ToString::to_string))
82    }
83
84    /// Get the input value of a form element.
85    ///
86    /// Works for `<input>`, `<textarea>`, and `<select>` elements.
87    ///
88    /// # Errors
89    ///
90    /// Returns an error if the element cannot be queried.
91    pub async fn input_value(&self) -> Result<String, LocatorError> {
92        // Handle Ref selector - lookup in ref map and resolve via CDP
93        if let Selector::Ref(ref_str) = &self.selector {
94            let backend_node_id = self.page.get_backend_node_id_for_ref(ref_str)?;
95            return self.input_value_by_backend_id(backend_node_id).await;
96        }
97
98        // Handle BackendNodeId selector
99        if let Selector::BackendNodeId(backend_node_id) = &self.selector {
100            return self.input_value_by_backend_id(*backend_node_id).await;
101        }
102
103        let selector_expr = self.selector.to_js_expression();
104        let js_code = js! {
105            (function() {
106                const elements = @{selector_expr};
107                if (elements.length === 0) return { found: false };
108                const el = elements[0];
109                return { found: true, value: el.value || "" };
110            })()
111        };
112
113        let result = self.evaluate_js(&js_code).await?;
114
115        let found = result
116            .get("found")
117            .and_then(serde_json::Value::as_bool)
118            .unwrap_or(false);
119        if !found {
120            return Err(LocatorError::NotFound(format!("{:?}", self.selector)));
121        }
122
123        Ok(result
124            .get("value")
125            .and_then(|v| v.as_str())
126            .unwrap_or("")
127            .to_string())
128    }
129
130    /// Get input value by backend node ID.
131    pub(super) async fn input_value_by_backend_id(
132        &self,
133        backend_node_id: BackendNodeId,
134    ) -> Result<String, LocatorError> {
135        let js_fn = js! {
136            (function() {
137                return { value: this.value || "" };
138            })
139        };
140        // Strip outer parentheses for CDP functionDeclaration
141        let js_fn = js_fn.trim_start_matches('(').trim_end_matches(')');
142
143        let result = self
144            .call_function_on_backend_id(backend_node_id, js_fn)
145            .await?;
146
147        Ok(result
148            .get("value")
149            .and_then(|v| v.as_str())
150            .unwrap_or("")
151            .to_string())
152    }
153}