viewpoint_core/page/file_chooser/
mod.rs

1//! File chooser handling for file upload dialogs.
2//!
3//! This module provides functionality for handling file chooser dialogs.
4
5// Allow dead code for file chooser scaffolding (spec: file-uploads)
6
7use std::path::Path;
8use std::sync::Arc;
9
10use viewpoint_cdp::protocol::dom::SetFileInputFilesParams;
11use viewpoint_cdp::CdpConnection;
12use tracing::{debug, instrument};
13
14use crate::error::LocatorError;
15
16/// A file chooser dialog.
17///
18/// File choosers are emitted via the `page.on_filechooser()` callback or can be
19/// obtained using `page.wait_for_file_chooser()`.
20///
21/// Note: You must call `page.set_intercept_file_chooser(true)` before the
22/// file chooser dialog is opened.
23///
24/// # Example
25///
26/// ```ignore
27/// // Enable file chooser interception
28/// page.set_intercept_file_chooser(true).await?;
29///
30/// let file_chooser = page.wait_for_file_chooser(async {
31///     page.locator("input[type=file]").click().await?;
32///     Ok(())
33/// }).await?;
34///
35/// // Set the files
36/// file_chooser.set_files(&["./upload.txt"]).await?;
37/// ```
38#[derive(Debug)]
39pub struct FileChooser {
40    /// CDP connection.
41    connection: Arc<CdpConnection>,
42    /// Session ID.
43    session_id: String,
44    /// Frame ID containing the file input.
45    frame_id: String,
46    /// Backend node ID of the file input element.
47    backend_node_id: Option<i32>,
48    /// Whether the file input accepts multiple files.
49    is_multiple: bool,
50}
51
52impl FileChooser {
53    /// Create a new `FileChooser`.
54    pub(crate) fn new(
55        connection: Arc<CdpConnection>,
56        session_id: String,
57        frame_id: String,
58        backend_node_id: Option<i32>,
59        is_multiple: bool,
60    ) -> Self {
61        Self {
62            connection,
63            session_id,
64            frame_id,
65            backend_node_id,
66            is_multiple,
67        }
68    }
69
70    /// Check if this file chooser accepts multiple files.
71    pub fn is_multiple(&self) -> bool {
72        self.is_multiple
73    }
74
75    /// Set the files to upload.
76    ///
77    /// # Arguments
78    ///
79    /// * `files` - Paths to the files to upload
80    ///
81    /// # Errors
82    ///
83    /// Returns an error if:
84    /// - Multiple files are provided but `is_multiple()` is false
85    /// - The CDP command fails
86    #[instrument(level = "debug", skip(self, files), fields(file_count = files.len()))]
87    pub async fn set_files<P: AsRef<Path>>(&self, files: &[P]) -> Result<(), LocatorError> {
88        if !self.is_multiple && files.len() > 1 {
89            return Err(LocatorError::EvaluationError(
90                "Cannot set multiple files on a single file input".to_string(),
91            ));
92        }
93
94        let file_paths: Vec<String> = files
95            .iter()
96            .map(|p| p.as_ref().to_string_lossy().into_owned())
97            .collect();
98
99        debug!("Setting {} files on file input", file_paths.len());
100
101        self.connection
102            .send_command::<_, serde_json::Value>(
103                "DOM.setFileInputFiles",
104                Some(SetFileInputFilesParams {
105                    files: file_paths,
106                    node_id: None,
107                    backend_node_id: self.backend_node_id,
108                    object_id: None,
109                }),
110                Some(&self.session_id),
111            )
112            .await?;
113
114        Ok(())
115    }
116}
117
118/// Represents a file to upload with its content.
119#[derive(Debug, Clone)]
120pub struct FilePayload {
121    /// File name.
122    pub name: String,
123    /// MIME type.
124    pub mime_type: String,
125    /// File content as bytes.
126    pub buffer: Vec<u8>,
127}
128
129impl FilePayload {
130    /// Create a new file payload.
131    pub fn new(name: impl Into<String>, mime_type: impl Into<String>, buffer: Vec<u8>) -> Self {
132        Self {
133            name: name.into(),
134            mime_type: mime_type.into(),
135            buffer,
136        }
137    }
138
139    /// Create a file payload from a string content.
140    pub fn from_text(name: impl Into<String>, content: impl Into<String>) -> Self {
141        Self {
142            name: name.into(),
143            mime_type: "text/plain".to_string(),
144            buffer: content.into().into_bytes(),
145        }
146    }
147
148    /// Create a file payload from JSON content.
149    pub fn from_json(name: impl Into<String>, content: impl Into<String>) -> Self {
150        Self {
151            name: name.into(),
152            mime_type: "application/json".to_string(),
153            buffer: content.into().into_bytes(),
154        }
155    }
156
157    /// Create a file payload from HTML content.
158    pub fn from_html(name: impl Into<String>, content: impl Into<String>) -> Self {
159        Self {
160            name: name.into(),
161            mime_type: "text/html".to_string(),
162            buffer: content.into().into_bytes(),
163        }
164    }
165}