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}