browser_commander/downloads/
attach.rs1use std::sync::Arc;
27
28use crate::core::EngineType;
29use crate::downloads::manager::DownloadManager;
30use crate::downloads::options::DownloadOptions;
31use crate::downloads::DownloadError;
32use crate::fingerprint::CdpTransport;
33
34#[derive(Debug, Clone, Default)]
40pub enum DownloadSetting {
41 #[default]
43 Off,
44 On,
46 Options(Box<DownloadOptions>),
48}
49
50impl From<bool> for DownloadSetting {
51 fn from(enabled: bool) -> Self {
52 if enabled {
53 Self::On
54 } else {
55 Self::Off
56 }
57 }
58}
59
60impl From<DownloadOptions> for DownloadSetting {
61 fn from(options: DownloadOptions) -> Self {
62 Self::Options(Box::new(options))
63 }
64}
65
66pub fn normalize_download_options(setting: DownloadSetting) -> Option<DownloadOptions> {
76 match setting {
77 DownloadSetting::Off => None,
78 DownloadSetting::On => Some(DownloadOptions::default()),
79 DownloadSetting::Options(options) => Some(*options),
80 }
81}
82
83pub fn supported_engine(engine: EngineType) -> Result<(), DownloadError> {
101 match engine {
102 EngineType::Chromiumoxide | EngineType::Fantoccini => Ok(()),
103 EngineType::Playwright | EngineType::Puppeteer => Err(DownloadError::Unsupported {
104 engine: engine.to_string(),
105 reason: "the node bridge speaks its own command protocol rather \
106 than CDP; use EngineType::Chromiumoxide, or manage \
107 downloads from the JavaScript package, which drives \
108 Playwright and Puppeteer directly"
109 .to_string(),
110 }),
111 }
112}
113
114pub async fn attach_downloads(
132 engine: EngineType,
133 transport: &dyn CdpTransport,
134 setting: DownloadSetting,
135) -> Result<Option<Arc<DownloadManager>>, DownloadError> {
136 let Some(options) = normalize_download_options(setting) else {
137 return Ok(None);
138 };
139 supported_engine(engine)?;
140 if engine == EngineType::Fantoccini {
141 return Err(DownloadError::Unsupported {
142 engine: engine.to_string(),
143 reason: "configure WebDriver download preferences before launch using launch_webdriver or LaunchOptions::fantoccini".into(),
144 });
145 }
146
147 let manager = DownloadManager::create(options)?;
148 manager.attach(transport).await?;
149 Ok(Some(manager))
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::downloads::test_support::{RecordingTransport, TempDir};
156
157 fn options(temp: &TempDir) -> DownloadOptions {
159 DownloadOptions::default().directory(temp.path().join("downloads").to_string_lossy())
160 }
161
162 #[test]
163 fn reads_the_three_shapes_the_other_languages_accept() {
164 assert!(normalize_download_options(DownloadSetting::Off).is_none());
165 assert!(normalize_download_options(false.into()).is_none());
166 assert!(normalize_download_options(DownloadSetting::On).is_some());
167 assert!(normalize_download_options(true.into()).is_some());
168
169 let configured = normalize_download_options(
170 DownloadOptions::default()
171 .directory("/tmp/bc-downloads")
172 .into(),
173 )
174 .expect("configured downloads");
175 assert_eq!(configured.directory.as_deref(), Some("/tmp/bc-downloads"));
176 }
177
178 #[tokio::test]
179 async fn points_the_browser_at_the_staging_directory() {
180 let temp = TempDir::new("bc-attach");
181 let transport = RecordingTransport::default();
182
183 let manager =
184 attach_downloads(EngineType::Chromiumoxide, &transport, options(&temp).into())
185 .await
186 .expect("attach")
187 .expect("a manager");
188
189 let sent = transport.sent();
190 assert_eq!(sent.len(), 1);
191 assert_eq!(sent[0].0, "Browser.setDownloadBehavior");
192 assert_eq!(
193 sent[0].1["downloadPath"],
194 manager
195 .directory
196 .join(crate::downloads::STAGING_DIRECTORY)
197 .to_string_lossy()
198 .as_ref()
199 );
200 manager.dispose().await;
201 }
202
203 #[tokio::test]
204 async fn leaves_the_browser_alone_when_downloads_are_off() {
205 let transport = RecordingTransport::default();
206
207 let manager = attach_downloads(EngineType::Chromiumoxide, &transport, DownloadSetting::Off)
208 .await
209 .expect("attach");
210
211 assert!(manager.is_none());
212 assert!(
213 transport.sent().is_empty(),
214 "a browser with downloads off had its download behavior changed"
215 );
216 }
217
218 #[tokio::test]
219 async fn refuses_an_engine_that_cannot_manage_downloads() {
220 let temp = TempDir::new("bc-attach-unsupported");
221 let transport = RecordingTransport::default();
222
223 for engine in [
224 EngineType::Fantoccini,
225 EngineType::Playwright,
226 EngineType::Puppeteer,
227 ] {
228 let error = attach_downloads(engine, &transport, options(&temp).into())
229 .await
230 .unwrap_err();
231
232 assert!(
233 error
234 .to_string()
235 .contains("managed downloads are not supported"),
236 "unexpected message: {error}"
237 );
238 assert!(
239 error.to_string().contains(&engine.to_string()),
240 "the message does not name the engine: {error}"
241 );
242 }
243 }
244
245 #[tokio::test]
246 async fn reports_a_browser_that_refuses_to_redirect_its_downloads() {
247 let temp = TempDir::new("bc-attach-refused");
248 let transport = RecordingTransport::refusing("Browser domain is not available");
249
250 let error = attach_downloads(EngineType::Chromiumoxide, &transport, options(&temp).into())
251 .await
252 .unwrap_err();
253
254 assert!(
255 error
256 .to_string()
257 .contains("the browser refused to redirect its downloads"),
258 "unexpected message: {error}"
259 );
260 }
261}