use std::path::PathBuf;
use crate::error::{Error, Result};
use super::core::Driver;
use super::profile::ExtensionSource;
#[derive(Debug, Default, Clone)]
pub struct DriverBuilder {
binary: Option<PathBuf>,
extension: Option<ExtensionSource>,
}
impl DriverBuilder {
#[inline]
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[inline]
#[must_use]
pub fn binary(mut self, path: impl Into<PathBuf>) -> Self {
self.binary = Some(path.into());
self
}
#[inline]
#[must_use]
pub fn extension(mut self, path: impl Into<PathBuf>) -> Self {
let path = path.into();
self.extension = Some(ExtensionSource::from(path));
self
}
#[inline]
#[must_use]
pub fn extension_base64(mut self, data: impl Into<String>) -> Self {
self.extension = Some(ExtensionSource::base64(data));
self
}
#[inline]
#[must_use]
pub fn extension_source(mut self, source: ExtensionSource) -> Self {
self.extension = Some(source);
self
}
pub async fn build(self) -> Result<Driver> {
let binary = self.validate_binary()?;
let extension = self.validate_extension()?;
Driver::new(binary, extension).await
}
}
impl DriverBuilder {
fn validate_binary(&self) -> Result<PathBuf> {
let binary = self.binary.clone().ok_or_else(|| {
Error::config(
"Firefox binary path is required. Use .binary() to set it.\n\
Example: Driver::builder().binary(\"/usr/bin/firefox\")",
)
})?;
if !binary.exists() {
return Err(Error::firefox_not_found(&binary));
}
Ok(binary)
}
fn validate_extension(&self) -> Result<ExtensionSource> {
let extension = self.extension.clone().ok_or_else(|| {
Error::config(
"Extension is required. Use .extension() or .extension_base64() to set it.\n\
Example: Driver::builder().extension(\"./extension\")",
)
})?;
if let Some(path) = extension.path()
&& !path.exists()
{
return Err(Error::config(format!(
"Extension not found at: {}\n\
Ensure the extension directory or .xpi file exists.",
path.display()
)));
}
Ok(extension)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_creates_empty_builder() {
let builder = DriverBuilder::new();
assert!(builder.binary.is_none());
assert!(builder.extension.is_none());
}
#[test]
fn test_default_creates_empty_builder() {
let builder = DriverBuilder::default();
assert!(builder.binary.is_none());
assert!(builder.extension.is_none());
}
#[test]
fn test_binary_sets_path() {
let builder = DriverBuilder::new().binary("/usr/bin/firefox");
assert_eq!(builder.binary, Some(PathBuf::from("/usr/bin/firefox")));
}
#[test]
fn test_extension_sets_source() {
let builder = DriverBuilder::new().extension("./extension");
assert!(builder.extension.is_some());
}
#[test]
fn test_extension_base64_sets_source() {
let builder = DriverBuilder::new().extension_base64("UEsDBBQ...");
assert!(builder.extension.is_some());
if let Some(ExtensionSource::Base64(data)) = builder.extension {
assert_eq!(data, "UEsDBBQ...");
} else {
panic!("Expected Base64 extension source");
}
}
#[test]
fn test_extension_source_sets_directly() {
let source = ExtensionSource::packed("./ext.xpi");
let builder = DriverBuilder::new().extension_source(source.clone());
assert_eq!(builder.extension, Some(source));
}
#[test]
fn test_build_fails_without_binary() {
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(DriverBuilder::new().extension("./extension").build());
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("binary"));
}
#[test]
fn test_build_fails_without_extension() {
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(DriverBuilder::new().binary("/bin/sh").build());
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Extension"));
}
#[test]
fn test_build_fails_with_nonexistent_binary() {
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(
DriverBuilder::new()
.binary("/nonexistent/firefox")
.extension_base64("data")
.build(),
);
assert!(result.is_err());
}
#[test]
fn test_builder_is_clone() {
let builder = DriverBuilder::new().binary("/usr/bin/firefox");
let cloned = builder.clone();
assert_eq!(builder.binary, cloned.binary);
}
}