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