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    /// ```ignore
23    /// // Set a single file
24    /// page.locator("input[type=file]").set_input_files(&["./upload.txt"]).await?;
25    ///
26    /// // Set multiple files
27    /// page.locator("input[type=file]").set_input_files(&["file1.txt", "file2.txt"]).await?;
28    ///
29    /// // Clear the file selection
30    /// page.locator("input[type=file]").set_input_files::<&str>(&[]).await?;
31    /// ```
32    #[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        // Get the element's backend node ID via JavaScript
44        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        // Use Runtime.evaluate to get the element object ID
76        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        // Set the files using DOM.setFileInputFiles
104        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    /// Set files on a file input element from memory buffers.
122    ///
123    /// This is useful when you want to upload files without having them on disk,
124    /// such as dynamically generated content or test data.
125    ///
126    /// # Example
127    ///
128    /// ```ignore
129    /// use viewpoint_core::FilePayload;
130    ///
131    /// // Upload a text file from memory
132    /// let payload = FilePayload::new("test.txt", "text/plain", b"Hello, World!".to_vec());
133    /// page.locator("input[type=file]").set_input_files_from_buffer(&[payload]).await?;
134    ///
135    /// // Upload multiple files
136    /// let file1 = FilePayload::from_text("doc1.txt", "Content 1");
137    /// let file2 = FilePayload::new("data.json", "application/json", r#"{"key": "value"}"#.as_bytes().to_vec());
138    /// page.locator("input[type=file]").set_input_files_from_buffer(&[file1, file2]).await?;
139    ///
140    /// // Clear files
141    /// page.locator("input[type=file]").set_input_files_from_buffer(&[]).await?;
142    /// ```
143    #[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        // Get the element's info via JavaScript
155        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        // Build the file data array for JavaScript
193        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        // Use JavaScript to create File objects and set them on the input
208        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}