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))]
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 #[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
74fn 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
114fn 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
180fn 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}