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>>(&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        // Get the element's backend node ID via JavaScript
49        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        // Use Runtime.evaluate to get the element object ID
81        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        // Set the files using DOM.setFileInputFiles
109        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    /// Set files on a file input element from memory buffers.
127    ///
128    /// This is useful when you want to upload files without having them on disk,
129    /// such as dynamically generated content or test data.
130    ///
131    /// # Example
132    ///
133    /// ```no_run
134    /// use viewpoint_core::{Page, FilePayload};
135    ///
136    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
137    /// // Upload a text file from memory
138    /// let payload = FilePayload::new("test.txt", "text/plain", b"Hello, World!".to_vec());
139    /// page.locator("input[type=file]").set_input_files_from_buffer(&[payload]).await?;
140    ///
141    /// // Upload multiple files
142    /// let file1 = FilePayload::from_text("doc1.txt", "Content 1");
143    /// let file2 = FilePayload::new("data.json", "application/json", r#"{"key": "value"}"#.as_bytes().to_vec());
144    /// page.locator("input[type=file]").set_input_files_from_buffer(&[file1, file2]).await?;
145    ///
146    /// // Clear files
147    /// page.locator("input[type=file]").set_input_files_from_buffer(&[]).await?;
148    /// # Ok(())
149    /// # }
150    /// ```
151    #[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        // Get the element's info via JavaScript
163        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        // Build the file data array for JavaScript
201        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        // Use JavaScript to create File objects and set them on the input
216        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}