viewpoint_core/page/locator/select/
mod.rs

1//! Select option methods for Locator.
2//!
3//! This module contains methods for selecting options in `<select>` elements.
4
5use tracing::{debug, instrument};
6
7use super::selector::js_string_literal;
8use super::Locator;
9use crate::error::LocatorError;
10
11impl Locator<'_> {
12    /// Select an option in a `<select>` element by value, label, or index.
13    ///
14    /// # Arguments
15    ///
16    /// * `option` - The option to select. Can be:
17    ///   - A string value matching the option's `value` attribute
18    ///   - A string matching the option's visible text
19    ///
20    /// # Errors
21    ///
22    /// Returns an error if the element is not a select or the option is not found.
23    ///
24    /// # Example
25    ///
26    /// ```no_run
27    /// use viewpoint_core::Page;
28    ///
29    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
30    /// // Select by value
31    /// page.locator("select#size").select_option("medium").await?;
32    ///
33    /// // Select by visible text
34    /// page.locator("select#size").select_option("Medium Size").await?;
35    /// # Ok(())
36    /// # }
37    /// ```
38    #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
39    pub async fn select_option(&self, option: &str) -> Result<(), LocatorError> {
40        self.wait_for_actionable().await?;
41
42        debug!(option, "Selecting option");
43
44        let js = build_select_option_js(&self.selector.to_js_expression(), option);
45        let result = self.evaluate_js(&js).await?;
46
47        check_select_result(&result)?;
48        Ok(())
49    }
50
51    /// Select multiple options in a `<select multiple>` element.
52    ///
53    /// # Arguments
54    ///
55    /// * `options` - A slice of option values or labels to select.
56    ///
57    /// # Errors
58    ///
59    /// Returns an error if the element is not a multi-select or options are not found.
60    #[instrument(level = "debug", skip(self, options), fields(selector = ?self.selector))]
61    pub async fn select_options(&self, options: &[&str]) -> Result<(), LocatorError> {
62        self.wait_for_actionable().await?;
63
64        debug!(?options, "Selecting multiple options");
65
66        let js = build_select_options_js(&self.selector.to_js_expression(), options);
67        let result = self.evaluate_js(&js).await?;
68
69        check_select_result(&result)?;
70        Ok(())
71    }
72}
73
74/// Build JavaScript for selecting a single option.
75fn build_select_option_js(selector_expr: &str, option: &str) -> String {
76    format!(
77        r"(function() {{
78            const elements = {selector};
79            if (elements.length === 0) return {{ success: false, error: 'Element not found' }};
80            
81            const select = elements[0];
82            if (select.tagName.toLowerCase() !== 'select') {{
83                return {{ success: false, error: 'Element is not a select' }};
84            }}
85            
86            const optionValue = {option};
87            
88            // Try to find by value first
89            for (let i = 0; i < select.options.length; i++) {{
90                if (select.options[i].value === optionValue) {{
91                    select.selectedIndex = i;
92                    select.dispatchEvent(new Event('change', {{ bubbles: true }}));
93                    return {{ success: true, selectedIndex: i, selectedValue: select.options[i].value }};
94                }}
95            }}
96            
97            // Try to find by text content
98            for (let i = 0; i < select.options.length; i++) {{
99                if (select.options[i].text === optionValue || 
100                    select.options[i].textContent.trim() === optionValue) {{
101                    select.selectedIndex = i;
102                    select.dispatchEvent(new Event('change', {{ bubbles: true }}));
103                    return {{ success: true, selectedIndex: i, selectedValue: select.options[i].value }};
104                }}
105            }}
106            
107            return {{ success: false, error: 'Option not found: ' + optionValue }};
108        }})()",
109        selector = selector_expr,
110        option = js_string_literal(option)
111    )
112}
113
114/// Build JavaScript for selecting multiple options.
115fn build_select_options_js(selector_expr: &str, options: &[&str]) -> String {
116    let options_js: Vec<String> = options.iter().map(|o| js_string_literal(o)).collect();
117    let options_array = format!("[{}]", options_js.join(", "));
118
119    format!(
120        r"(function() {{
121            const elements = {selector_expr};
122            if (elements.length === 0) return {{ success: false, error: 'Element not found' }};
123            
124            const select = elements[0];
125            if (select.tagName.toLowerCase() !== 'select') {{
126                return {{ success: false, error: 'Element is not a select' }};
127            }}
128            
129            const optionValues = {options_array};
130            const selectedIndices = [];
131            
132            // Clear current selection if not multiple
133            if (!select.multiple) {{
134                return {{ success: false, error: 'select_options requires a <select multiple>' }};
135            }}
136            
137            // Deselect all first
138            for (let i = 0; i < select.options.length; i++) {{
139                select.options[i].selected = false;
140            }}
141            
142            // Select each requested option
143            for (const optionValue of optionValues) {{
144                let found = false;
145                
146                // Try to find by value
147                for (let i = 0; i < select.options.length; i++) {{
148                    if (select.options[i].value === optionValue) {{
149                        select.options[i].selected = true;
150                        selectedIndices.push(i);
151                        found = true;
152                        break;
153                    }}
154                }}
155                
156                // Try to find by text if not found by value
157                if (!found) {{
158                    for (let i = 0; i < select.options.length; i++) {{
159                        if (select.options[i].text === optionValue || 
160                            select.options[i].textContent.trim() === optionValue) {{
161                            select.options[i].selected = true;
162                            selectedIndices.push(i);
163                            found = true;
164                            break;
165                        }}
166                    }}
167                }}
168                
169                if (!found) {{
170                    return {{ success: false, error: 'Option not found: ' + optionValue }};
171                }}
172            }}
173            
174            select.dispatchEvent(new Event('change', {{ bubbles: true }}));
175            return {{ success: true, selectedIndices: selectedIndices }};
176        }})()"
177    )
178}
179
180/// Check the result of a select operation.
181fn check_select_result(result: &serde_json::Value) -> Result<(), LocatorError> {
182    let success = result
183        .get("success")
184        .and_then(serde_json::Value::as_bool)
185        .unwrap_or(false);
186
187    if !success {
188        let error = result
189            .get("error")
190            .and_then(|v| v.as_str())
191            .unwrap_or("Unknown error");
192        return Err(LocatorError::EvaluationError(error.to_string()));
193    }
194
195    Ok(())
196}