viewpoint_core/page/locator/files/
mod.rs

1//! File handling methods for locators.
2//!
3//! Methods for setting files on `<input type="file">` elements.
4
5use tracing::{debug, instrument};
6
7use super::Locator;
8use crate::error::LocatorError;
9
10impl Locator<'_> {
11    /// Set files on an `<input type="file">` element.
12    ///
13    /// This is the recommended way to upload files. Use an empty slice to clear
14    /// the file selection.
15    ///
16    /// # Arguments
17    ///
18    /// * `files` - Paths to the files to upload. Pass an empty slice to clear.
19    ///
20    /// # Example
21    ///
22    /// ```no_run
23    /// use viewpoint_core::Page;
24    ///
25    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
26    /// // Set a single file
27    /// page.locator("input[type=file]").set_input_files(&["./upload.txt"]).await?;
28    ///
29    /// // Set multiple files
30    /// page.locator("input[type=file]").set_input_files(&["file1.txt", "file2.txt"]).await?;
31    ///
32    /// // Clear the file selection
33    /// page.locator("input[type=file]").set_input_files::<&str>(&[]).await?;
34    /// # Ok(())
35    /// # }
36    /// ```
37    #[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        // Get the element's backend node ID via JavaScript
52        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        // Use Runtime.evaluate to get the element object ID
93        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        // Set the files using DOM.setFileInputFiles
126        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    /// Set files on a file input element from memory buffers.
144    ///
145    /// This is useful when you want to upload files without having them on disk,
146    /// such as dynamically generated content or test data.
147    ///
148    /// # Example
149    ///
150    /// ```no_run
151    /// use viewpoint_core::{Page, FilePayload};
152    ///
153    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
154    /// // Upload a text file from memory
155    /// let payload = FilePayload::new("test.txt", "text/plain", b"Hello, World!".to_vec());
156    /// page.locator("input[type=file]").set_input_files_from_buffer(&[payload]).await?;
157    ///
158    /// // Upload multiple files
159    /// let file1 = FilePayload::from_text("doc1.txt", "Content 1");
160    /// let file2 = FilePayload::new("data.json", "application/json", r#"{"key": "value"}"#.as_bytes().to_vec());
161    /// page.locator("input[type=file]").set_input_files_from_buffer(&[file1, file2]).await?;
162    ///
163    /// // Clear files
164    /// page.locator("input[type=file]").set_input_files_from_buffer(&[]).await?;
165    /// # Ok(())
166    /// # }
167    /// ```
168    #[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        // Get the element's info via JavaScript
180        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        // Build the file data array for JavaScript
221        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        // Use JavaScript to create File objects and set them on the input
236        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}