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/// ```
27/// # #[cfg(feature = "integration")]
28/// # tokio_test::block_on(async {
29/// # use viewpoint_core::Browser;
30/// # let browser = Browser::launch().headless(true).launch().await.unwrap();
31/// # let context = browser.new_context().await.unwrap();
32/// # let page = context.new_page().await.unwrap();
33///
34/// // Enable file chooser interception
35/// page.set_intercept_file_chooser(true).await.unwrap();
36///
37/// // File chooser would be obtained like:
38/// // let file_chooser = page.wait_for_file_chooser(async {
39/// //     page.locator("input[type=file]").click().await?;
40/// //     Ok(())
41/// // }).await?;
42/// # });
43/// ```
44#[derive(Debug)]
45pub struct FileChooser {
46    /// CDP connection.
47    connection: Arc<CdpConnection>,
48    /// Session ID.
49    session_id: String,
50    /// Frame ID containing the file input.
51    frame_id: String,
52    /// Backend node ID of the file input element.
53    backend_node_id: Option<i32>,
54    /// Whether the file input accepts multiple files.
55    is_multiple: bool,
56}
57
58impl FileChooser {
59    /// Create a new `FileChooser`.
60    pub(crate) fn new(
61        connection: Arc<CdpConnection>,
62        session_id: String,
63        frame_id: String,
64        backend_node_id: Option<i32>,
65        is_multiple: bool,
66    ) -> Self {
67        Self {
68            connection,
69            session_id,
70            frame_id,
71            backend_node_id,
72            is_multiple,
73        }
74    }
75
76    /// Check if this file chooser accepts multiple files.
77    pub fn is_multiple(&self) -> bool {
78        self.is_multiple
79    }
80
81    /// Set the files to upload.
82    ///
83    /// # Arguments
84    ///
85    /// * `files` - Paths to the files to upload
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if:
90    /// - Multiple files are provided but `is_multiple()` is false
91    /// - The CDP command fails
92    #[instrument(level = "debug", skip(self, files), fields(file_count = files.len()))]
93    pub async fn set_files<P: AsRef<Path>>(&self, files: &[P]) -> Result<(), LocatorError> {
94        if !self.is_multiple && files.len() > 1 {
95            return Err(LocatorError::EvaluationError(
96                "Cannot set multiple files on a single file input".to_string(),
97            ));
98        }
99
100        let file_paths: Vec<String> = files
101            .iter()
102            .map(|p| p.as_ref().to_string_lossy().into_owned())
103            .collect();
104
105        debug!("Setting {} files on file input", file_paths.len());
106
107        self.connection
108            .send_command::<_, serde_json::Value>(
109                "DOM.setFileInputFiles",
110                Some(SetFileInputFilesParams {
111                    files: file_paths,
112                    node_id: None,
113                    backend_node_id: self.backend_node_id,
114                    object_id: None,
115                }),
116                Some(&self.session_id),
117            )
118            .await?;
119
120        Ok(())
121    }
122}
123
124/// Represents a file to upload with its content.
125#[derive(Debug, Clone)]
126pub struct FilePayload {
127    /// File name.
128    pub name: String,
129    /// MIME type.
130    pub mime_type: String,
131    /// File content as bytes.
132    pub buffer: Vec<u8>,
133}
134
135impl FilePayload {
136    /// Create a new file payload.
137    pub fn new(name: impl Into<String>, mime_type: impl Into<String>, buffer: Vec<u8>) -> Self {
138        Self {
139            name: name.into(),
140            mime_type: mime_type.into(),
141            buffer,
142        }
143    }
144
145    /// Create a file payload from a string content.
146    pub fn from_text(name: impl Into<String>, content: impl Into<String>) -> Self {
147        Self {
148            name: name.into(),
149            mime_type: "text/plain".to_string(),
150            buffer: content.into().into_bytes(),
151        }
152    }
153
154    /// Create a file payload from JSON content.
155    pub fn from_json(name: impl Into<String>, content: impl Into<String>) -> Self {
156        Self {
157            name: name.into(),
158            mime_type: "application/json".to_string(),
159            buffer: content.into().into_bytes(),
160        }
161    }
162
163    /// Create a file payload from HTML content.
164    pub fn from_html(name: impl Into<String>, content: impl Into<String>) -> Self {
165        Self {
166            name: name.into(),
167            mime_type: "text/html".to_string(),
168            buffer: content.into().into_bytes(),
169        }
170    }
171}