use std::sync::Arc;
use crate::core::EngineType;
use crate::downloads::manager::DownloadManager;
use crate::downloads::options::DownloadOptions;
use crate::downloads::DownloadError;
use crate::fingerprint::CdpTransport;
#[derive(Debug, Clone, Default)]
pub enum DownloadSetting {
#[default]
Off,
On,
Options(Box<DownloadOptions>),
}
impl From<bool> for DownloadSetting {
fn from(enabled: bool) -> Self {
if enabled {
Self::On
} else {
Self::Off
}
}
}
impl From<DownloadOptions> for DownloadSetting {
fn from(options: DownloadOptions) -> Self {
Self::Options(Box::new(options))
}
}
pub fn normalize_download_options(setting: DownloadSetting) -> Option<DownloadOptions> {
match setting {
DownloadSetting::Off => None,
DownloadSetting::On => Some(DownloadOptions::default()),
DownloadSetting::Options(options) => Some(*options),
}
}
pub fn supported_engine(engine: EngineType) -> Result<(), DownloadError> {
match engine {
EngineType::Chromiumoxide | EngineType::Fantoccini => Ok(()),
EngineType::Playwright | EngineType::Puppeteer => Err(DownloadError::Unsupported {
engine: engine.to_string(),
reason: "the node bridge speaks its own command protocol rather \
than CDP; use EngineType::Chromiumoxide, or manage \
downloads from the JavaScript package, which drives \
Playwright and Puppeteer directly"
.to_string(),
}),
}
}
pub async fn attach_downloads(
engine: EngineType,
transport: &dyn CdpTransport,
setting: DownloadSetting,
) -> Result<Option<Arc<DownloadManager>>, DownloadError> {
let Some(options) = normalize_download_options(setting) else {
return Ok(None);
};
supported_engine(engine)?;
if engine == EngineType::Fantoccini {
return Err(DownloadError::Unsupported {
engine: engine.to_string(),
reason: "configure WebDriver download preferences before launch using launch_webdriver or LaunchOptions::fantoccini".into(),
});
}
let manager = DownloadManager::create(options)?;
manager.attach(transport).await?;
Ok(Some(manager))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::downloads::test_support::{RecordingTransport, TempDir};
fn options(temp: &TempDir) -> DownloadOptions {
DownloadOptions::default().directory(temp.path().join("downloads").to_string_lossy())
}
#[test]
fn reads_the_three_shapes_the_other_languages_accept() {
assert!(normalize_download_options(DownloadSetting::Off).is_none());
assert!(normalize_download_options(false.into()).is_none());
assert!(normalize_download_options(DownloadSetting::On).is_some());
assert!(normalize_download_options(true.into()).is_some());
let configured = normalize_download_options(
DownloadOptions::default()
.directory("/tmp/bc-downloads")
.into(),
)
.expect("configured downloads");
assert_eq!(configured.directory.as_deref(), Some("/tmp/bc-downloads"));
}
#[tokio::test]
async fn points_the_browser_at_the_staging_directory() {
let temp = TempDir::new("bc-attach");
let transport = RecordingTransport::default();
let manager =
attach_downloads(EngineType::Chromiumoxide, &transport, options(&temp).into())
.await
.expect("attach")
.expect("a manager");
let sent = transport.sent();
assert_eq!(sent.len(), 1);
assert_eq!(sent[0].0, "Browser.setDownloadBehavior");
assert_eq!(
sent[0].1["downloadPath"],
manager
.directory
.join(crate::downloads::STAGING_DIRECTORY)
.to_string_lossy()
.as_ref()
);
manager.dispose().await;
}
#[tokio::test]
async fn leaves_the_browser_alone_when_downloads_are_off() {
let transport = RecordingTransport::default();
let manager = attach_downloads(EngineType::Chromiumoxide, &transport, DownloadSetting::Off)
.await
.expect("attach");
assert!(manager.is_none());
assert!(
transport.sent().is_empty(),
"a browser with downloads off had its download behavior changed"
);
}
#[tokio::test]
async fn refuses_an_engine_that_cannot_manage_downloads() {
let temp = TempDir::new("bc-attach-unsupported");
let transport = RecordingTransport::default();
for engine in [
EngineType::Fantoccini,
EngineType::Playwright,
EngineType::Puppeteer,
] {
let error = attach_downloads(engine, &transport, options(&temp).into())
.await
.unwrap_err();
assert!(
error
.to_string()
.contains("managed downloads are not supported"),
"unexpected message: {error}"
);
assert!(
error.to_string().contains(&engine.to_string()),
"the message does not name the engine: {error}"
);
}
}
#[tokio::test]
async fn reports_a_browser_that_refuses_to_redirect_its_downloads() {
let temp = TempDir::new("bc-attach-refused");
let transport = RecordingTransport::refusing("Browser domain is not available");
let error = attach_downloads(EngineType::Chromiumoxide, &transport, options(&temp).into())
.await
.unwrap_err();
assert!(
error
.to_string()
.contains("the browser refused to redirect its downloads"),
"unexpected message: {error}"
);
}
}