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