viewpoint_core/page/locator/select/
mod.rs1use tracing::{debug, instrument};
6
7use super::selector::js_string_literal;
8use super::Locator;
9use crate::error::LocatorError;
10
11impl Locator<'_> {
12 #[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 #[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
69fn 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
109fn 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
175fn 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}