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