viewpoint_core/page/locator/files/
mod.rs1use tracing::{debug, instrument};
6
7use super::Locator;
8use crate::error::LocatorError;
9
10impl Locator<'_> {
11 #[instrument(level = "debug", skip(self, files), fields(selector = ?self.selector, file_count = files.len()))]
38 pub async fn set_input_files<P: AsRef<std::path::Path>>(
39 &self,
40 files: &[P],
41 ) -> Result<(), LocatorError> {
42 self.wait_for_actionable().await?;
43
44 let file_paths: Vec<String> = files
45 .iter()
46 .map(|p| p.as_ref().to_string_lossy().into_owned())
47 .collect();
48
49 debug!("Setting {} files on file input", file_paths.len());
50
51 let js = format!(
53 r"(function() {{
54 const elements = {selector};
55 if (elements.length === 0) return {{ found: false, error: 'Element not found' }};
56
57 const el = elements[0];
58 if (el.tagName.toLowerCase() !== 'input' || el.type !== 'file') {{
59 return {{ found: false, error: 'Element is not a file input' }};
60 }}
61
62 return {{ found: true, isMultiple: el.multiple }};
63 }})()",
64 selector = self.selector.to_js_expression()
65 );
66
67 let result = self.evaluate_js(&js).await?;
68
69 let found = result
70 .get("found")
71 .and_then(serde_json::Value::as_bool)
72 .unwrap_or(false);
73 if !found {
74 let error = result
75 .get("error")
76 .and_then(|v| v.as_str())
77 .unwrap_or("Unknown error");
78 return Err(LocatorError::EvaluationError(error.to_string()));
79 }
80
81 let is_multiple = result
82 .get("isMultiple")
83 .and_then(serde_json::Value::as_bool)
84 .unwrap_or(false);
85
86 if !is_multiple && file_paths.len() > 1 {
87 return Err(LocatorError::EvaluationError(
88 "Cannot set multiple files on a single file input".to_string(),
89 ));
90 }
91
92 let get_object_js = format!(
94 r"(function() {{
95 const elements = {selector};
96 return elements[0];
97 }})()",
98 selector = self.selector.to_js_expression()
99 );
100
101 let params = viewpoint_cdp::protocol::runtime::EvaluateParams {
102 expression: get_object_js,
103 object_group: Some("viewpoint-file-input".to_string()),
104 include_command_line_api: None,
105 silent: Some(true),
106 context_id: None,
107 return_by_value: Some(false),
108 await_promise: Some(false),
109 };
110
111 let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
112 .page
113 .connection()
114 .send_command(
115 "Runtime.evaluate",
116 Some(params),
117 Some(self.page.session_id()),
118 )
119 .await?;
120
121 let object_id = result.result.object_id.ok_or_else(|| {
122 LocatorError::EvaluationError("Failed to get element object ID".to_string())
123 })?;
124
125 self.page
127 .connection()
128 .send_command::<_, serde_json::Value>(
129 "DOM.setFileInputFiles",
130 Some(viewpoint_cdp::protocol::dom::SetFileInputFilesParams {
131 files: file_paths,
132 node_id: None,
133 backend_node_id: None,
134 object_id: Some(object_id),
135 }),
136 Some(self.page.session_id()),
137 )
138 .await?;
139
140 Ok(())
141 }
142
143 #[instrument(level = "debug", skip(self, files), fields(selector = ?self.selector, file_count = files.len()))]
169 pub async fn set_input_files_from_buffer(
170 &self,
171 files: &[crate::page::FilePayload],
172 ) -> Result<(), LocatorError> {
173 use base64::{Engine, engine::general_purpose::STANDARD};
174
175 self.wait_for_actionable().await?;
176
177 debug!("Setting {} files from buffer on file input", files.len());
178
179 let js = format!(
181 r"(function() {{
182 const elements = {selector};
183 if (elements.length === 0) return {{ found: false, error: 'Element not found' }};
184
185 const el = elements[0];
186 if (el.tagName.toLowerCase() !== 'input' || el.type !== 'file') {{
187 return {{ found: false, error: 'Element is not a file input' }};
188 }}
189
190 return {{ found: true, isMultiple: el.multiple }};
191 }})()",
192 selector = self.selector.to_js_expression()
193 );
194
195 let result = self.evaluate_js(&js).await?;
196
197 let found = result
198 .get("found")
199 .and_then(serde_json::Value::as_bool)
200 .unwrap_or(false);
201 if !found {
202 let error = result
203 .get("error")
204 .and_then(|v| v.as_str())
205 .unwrap_or("Unknown error");
206 return Err(LocatorError::EvaluationError(error.to_string()));
207 }
208
209 let is_multiple = result
210 .get("isMultiple")
211 .and_then(serde_json::Value::as_bool)
212 .unwrap_or(false);
213
214 if !is_multiple && files.len() > 1 {
215 return Err(LocatorError::EvaluationError(
216 "Cannot set multiple files on a single file input".to_string(),
217 ));
218 }
219
220 let file_data: Vec<serde_json::Value> = files
222 .iter()
223 .map(|f| {
224 serde_json::json!({
225 "name": f.name,
226 "mimeType": f.mime_type,
227 "data": STANDARD.encode(&f.buffer),
228 })
229 })
230 .collect();
231
232 let file_data_json = serde_json::to_string(&file_data)
233 .map_err(|e| LocatorError::EvaluationError(e.to_string()))?;
234
235 let set_files_js = format!(
237 r"(async function() {{
238 const elements = {selector};
239 if (elements.length === 0) return {{ success: false, error: 'Element not found' }};
240
241 const input = elements[0];
242 const fileData = {file_data};
243
244 // Create File objects from the data
245 const files = await Promise.all(fileData.map(async (fd) => {{
246 // Decode base64 to binary
247 const binaryString = atob(fd.data);
248 const bytes = new Uint8Array(binaryString.length);
249 for (let i = 0; i < binaryString.length; i++) {{
250 bytes[i] = binaryString.charCodeAt(i);
251 }}
252
253 return new File([bytes], fd.name, {{ type: fd.mimeType }});
254 }}));
255
256 // Create a DataTransfer to hold the files
257 const dataTransfer = new DataTransfer();
258 for (const file of files) {{
259 dataTransfer.items.add(file);
260 }}
261
262 // Set the files on the input
263 input.files = dataTransfer.files;
264
265 // Dispatch change event
266 input.dispatchEvent(new Event('change', {{ bubbles: true }}));
267 input.dispatchEvent(new Event('input', {{ bubbles: true }}));
268
269 return {{ success: true }};
270 }})()",
271 selector = self.selector.to_js_expression(),
272 file_data = file_data_json
273 );
274
275 let params = viewpoint_cdp::protocol::runtime::EvaluateParams {
276 expression: set_files_js,
277 object_group: Some("viewpoint-file-input".to_string()),
278 include_command_line_api: None,
279 silent: Some(false),
280 context_id: None,
281 return_by_value: Some(true),
282 await_promise: Some(true),
283 };
284
285 let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
286 .page
287 .connection()
288 .send_command(
289 "Runtime.evaluate",
290 Some(params),
291 Some(self.page.session_id()),
292 )
293 .await?;
294
295 if let Some(exception) = result.exception_details {
296 return Err(LocatorError::EvaluationError(format!(
297 "Failed to set files: {}",
298 exception.text
299 )));
300 }
301
302 if let Some(value) = result.result.value {
303 let success = value
304 .get("success")
305 .and_then(serde_json::Value::as_bool)
306 .unwrap_or(false);
307 if !success {
308 let error = value
309 .get("error")
310 .and_then(|v| v.as_str())
311 .unwrap_or("Unknown error");
312 return Err(LocatorError::EvaluationError(error.to_string()));
313 }
314 }
315
316 Ok(())
317 }
318}