html2pdf_api/
config.rs

1//! Configuration for browser pool behavior and limits.
2//!
3//! This module provides [`BrowserPoolConfig`] and [`BrowserPoolConfigBuilder`]
4//! for configuring pool size, browser lifecycle, and health monitoring parameters.
5//!
6//! # Example
7//!
8//! ```rust
9//! use std::time::Duration;
10//! use html2pdf_api::BrowserPoolConfigBuilder;
11//!
12//! let config = BrowserPoolConfigBuilder::new()
13//!     .max_pool_size(10)
14//!     .warmup_count(5)
15//!     .browser_ttl(Duration::from_secs(7200))
16//!     .build()
17//!     .expect("Invalid configuration");
18//!
19//! assert_eq!(config.max_pool_size, 10);
20//! assert_eq!(config.warmup_count, 5);
21//! ```
22//!
23//! # Environment Configuration
24//!
25//! When the `env-config` feature is enabled, you can load configuration
26//! from environment variables and an optional `app.env` file:
27//!
28//! ```rust,ignore
29//! use html2pdf_api::config::env::from_env;
30//!
31//! let config = from_env()?;
32//! ```
33//!
34//! See [`mod@env`] module for available environment variables.
35
36use std::time::Duration;
37
38/// Configuration for browser pool behavior and limits.
39///
40/// Controls pool size, browser lifecycle, and health monitoring parameters.
41/// Use [`BrowserPoolConfigBuilder`] for validation and convenience.
42///
43/// # Fields Overview
44///
45/// | Field | Default | Description |
46/// |-------|---------|-------------|
47/// | `max_pool_size` | 5 | Maximum browsers in pool |
48/// | `warmup_count` | 3 | Browsers to pre-create |
49/// | `ping_interval` | 15s | Health check frequency |
50/// | `browser_ttl` | 1 hour | Browser lifetime |
51/// | `max_ping_failures` | 3 | Failures before removal |
52/// | `warmup_timeout` | 60s | Warmup time limit |
53///
54/// # Example
55///
56/// ```rust
57/// use html2pdf_api::BrowserPoolConfig;
58///
59/// // Use defaults
60/// let config = BrowserPoolConfig::default();
61/// assert_eq!(config.max_pool_size, 5);
62/// ```
63#[derive(Debug, Clone)]
64pub struct BrowserPoolConfig {
65    /// Maximum number of browsers to keep in the pool (idle + active).
66    ///
67    /// This is a soft limit - active browsers may temporarily exceed this during high load.
68    ///
69    /// # Default
70    ///
71    /// 5 browsers
72    ///
73    /// # Considerations
74    ///
75    /// - Higher values = more memory usage, better concurrency
76    /// - Lower values = less memory, potential queuing under load
77    pub max_pool_size: usize,
78
79    /// Number of browsers to pre-create during warmup phase.
80    ///
81    /// Must be d `max_pool_size`. Reduces first-request latency.
82    ///
83    /// # Default
84    ///
85    /// 3 browsers
86    ///
87    /// # Considerations
88    ///
89    /// - Set to `max_pool_size` for fastest first requests
90    /// - Set to 0 for lazy initialization (browsers created on demand)
91    pub warmup_count: usize,
92
93    /// Interval between health check pings for active browsers.
94    ///
95    /// Shorter intervals = faster failure detection, higher overhead.
96    ///
97    /// # Default
98    ///
99    /// 15 seconds
100    ///
101    /// # Considerations
102    ///
103    /// - Too short: Unnecessary CPU/memory overhead
104    /// - Too long: Slow detection of crashed browsers
105    pub ping_interval: Duration,
106
107    /// Time-to-live for each browser instance before forced retirement.
108    ///
109    /// Prevents memory leaks from long-running browser processes.
110    ///
111    /// # Default
112    ///
113    /// 1 hour (3600 seconds)
114    ///
115    /// # Considerations
116    ///
117    /// - Chrome can accumulate memory over time
118    /// - Shorter TTL = more browser restarts, fresher instances
119    /// - Longer TTL = fewer restarts, potential memory growth
120    pub browser_ttl: Duration,
121
122    /// Maximum consecutive ping failures before removing a browser.
123    ///
124    /// Higher values = more tolerance for transient failures.
125    ///
126    /// # Default
127    ///
128    /// 3 consecutive failures
129    ///
130    /// # Considerations
131    ///
132    /// - Set to 1 for aggressive failure detection
133    /// - Set higher if experiencing transient network issues
134    pub max_ping_failures: u32,
135
136    /// Maximum time allowed for warmup process to complete.
137    ///
138    /// If warmup doesn't complete in this time, it fails with timeout error.
139    ///
140    /// # Default
141    ///
142    /// 60 seconds
143    ///
144    /// # Considerations
145    ///
146    /// - Should be at least `warmup_count * ~5 seconds` per browser
147    /// - Increase if running on slow hardware or with many warmup browsers
148    pub warmup_timeout: Duration,
149}
150
151impl Default for BrowserPoolConfig {
152    /// Production-ready default configuration.
153    ///
154    /// - Pool size: 5 browsers
155    /// - Warmup: 3 browsers
156    /// - Health checks: Every 15 seconds
157    /// - TTL: 1 hour
158    /// - Failure tolerance: 3 consecutive failures
159    /// - Warmup timeout: 60 seconds
160    ///
161    /// # Example
162    ///
163    /// ```rust
164    /// use html2pdf_api::BrowserPoolConfig;
165    /// use std::time::Duration;
166    ///
167    /// let config = BrowserPoolConfig::default();
168    ///
169    /// assert_eq!(config.max_pool_size, 5);
170    /// assert_eq!(config.warmup_count, 3);
171    /// assert_eq!(config.ping_interval, Duration::from_secs(15));
172    /// assert_eq!(config.browser_ttl, Duration::from_secs(3600));
173    /// assert_eq!(config.max_ping_failures, 3);
174    /// assert_eq!(config.warmup_timeout, Duration::from_secs(60));
175    /// ```
176    fn default() -> Self {
177        Self {
178            max_pool_size: 5,
179            warmup_count: 3,
180            ping_interval: Duration::from_secs(15),
181            browser_ttl: Duration::from_secs(3600), // 1 hour
182            max_ping_failures: 3,
183            warmup_timeout: Duration::from_secs(60),
184        }
185    }
186}
187
188/// Builder for [`BrowserPoolConfig`] with validation.
189///
190/// Provides a fluent API for constructing validated configurations.
191/// All setter methods can be chained together.
192///
193/// # Example
194///
195/// ```rust
196/// use std::time::Duration;
197/// use html2pdf_api::BrowserPoolConfigBuilder;
198///
199/// let config = BrowserPoolConfigBuilder::new()
200///     .max_pool_size(10)
201///     .warmup_count(5)
202///     .browser_ttl(Duration::from_secs(7200))
203///     .build()
204///     .expect("Invalid configuration");
205/// ```
206///
207/// # Validation
208///
209/// The [`build()`](Self::build) method validates:
210/// - `max_pool_size` must be greater than 0
211/// - `warmup_count` must be d `max_pool_size`
212pub struct BrowserPoolConfigBuilder {
213    config: BrowserPoolConfig,
214}
215
216impl BrowserPoolConfigBuilder {
217    /// Create a new builder with default values.
218    ///
219    /// # Example
220    ///
221    /// ```rust
222    /// use html2pdf_api::BrowserPoolConfigBuilder;
223    ///
224    /// let builder = BrowserPoolConfigBuilder::new();
225    /// let config = builder.build().unwrap();
226    ///
227    /// // Has default values
228    /// assert_eq!(config.max_pool_size, 5);
229    /// ```
230    pub fn new() -> Self {
231        Self {
232            config: BrowserPoolConfig::default(),
233        }
234    }
235
236    /// Set maximum pool size (must be > 0).
237    ///
238    /// # Parameters
239    ///
240    /// * `size` - Maximum number of browsers in the pool.
241    ///
242    /// # Example
243    ///
244    /// ```rust
245    /// use html2pdf_api::BrowserPoolConfigBuilder;
246    ///
247    /// let config = BrowserPoolConfigBuilder::new()
248    ///     .max_pool_size(10)
249    ///     .build()
250    ///     .unwrap();
251    ///
252    /// assert_eq!(config.max_pool_size, 10);
253    /// ```
254    pub fn max_pool_size(mut self, size: usize) -> Self {
255        self.config.max_pool_size = size;
256        self
257    }
258
259    /// Set warmup count (must be d max_pool_size).
260    ///
261    /// # Parameters
262    ///
263    /// * `count` - Number of browsers to pre-create during warmup.
264    ///
265    /// # Example
266    ///
267    /// ```rust
268    /// use html2pdf_api::BrowserPoolConfigBuilder;
269    ///
270    /// let config = BrowserPoolConfigBuilder::new()
271    ///     .max_pool_size(10)
272    ///     .warmup_count(5)
273    ///     .build()
274    ///     .unwrap();
275    ///
276    /// assert_eq!(config.warmup_count, 5);
277    /// ```
278    pub fn warmup_count(mut self, count: usize) -> Self {
279        self.config.warmup_count = count;
280        self
281    }
282
283    /// Set health check interval.
284    ///
285    /// # Parameters
286    ///
287    /// * `interval` - Duration between health check pings.
288    ///
289    /// # Example
290    ///
291    /// ```rust
292    /// use std::time::Duration;
293    /// use html2pdf_api::BrowserPoolConfigBuilder;
294    ///
295    /// let config = BrowserPoolConfigBuilder::new()
296    ///     .ping_interval(Duration::from_secs(30))
297    ///     .build()
298    ///     .unwrap();
299    ///
300    /// assert_eq!(config.ping_interval, Duration::from_secs(30));
301    /// ```
302    pub fn ping_interval(mut self, interval: Duration) -> Self {
303        self.config.ping_interval = interval;
304        self
305    }
306
307    /// Set browser time-to-live before forced retirement.
308    ///
309    /// # Parameters
310    ///
311    /// * `ttl` - Maximum lifetime for each browser instance.
312    ///
313    /// # Example
314    ///
315    /// ```rust
316    /// use std::time::Duration;
317    /// use html2pdf_api::BrowserPoolConfigBuilder;
318    ///
319    /// let config = BrowserPoolConfigBuilder::new()
320    ///     .browser_ttl(Duration::from_secs(7200)) // 2 hours
321    ///     .build()
322    ///     .unwrap();
323    ///
324    /// assert_eq!(config.browser_ttl, Duration::from_secs(7200));
325    /// ```
326    pub fn browser_ttl(mut self, ttl: Duration) -> Self {
327        self.config.browser_ttl = ttl;
328        self
329    }
330
331    /// Set maximum consecutive ping failures before removal.
332    ///
333    /// # Parameters
334    ///
335    /// * `failures` - Number of consecutive failures tolerated.
336    ///
337    /// # Example
338    ///
339    /// ```rust
340    /// use html2pdf_api::BrowserPoolConfigBuilder;
341    ///
342    /// let config = BrowserPoolConfigBuilder::new()
343    ///     .max_ping_failures(5)
344    ///     .build()
345    ///     .unwrap();
346    ///
347    /// assert_eq!(config.max_ping_failures, 5);
348    /// ```
349    pub fn max_ping_failures(mut self, failures: u32) -> Self {
350        self.config.max_ping_failures = failures;
351        self
352    }
353
354    /// Set warmup timeout.
355    ///
356    /// # Parameters
357    ///
358    /// * `timeout` - Maximum time allowed for warmup to complete.
359    ///
360    /// # Example
361    ///
362    /// ```rust
363    /// use std::time::Duration;
364    /// use html2pdf_api::BrowserPoolConfigBuilder;
365    ///
366    /// let config = BrowserPoolConfigBuilder::new()
367    ///     .warmup_timeout(Duration::from_secs(120))
368    ///     .build()
369    ///     .unwrap();
370    ///
371    /// assert_eq!(config.warmup_timeout, Duration::from_secs(120));
372    /// ```
373    pub fn warmup_timeout(mut self, timeout: Duration) -> Self {
374        self.config.warmup_timeout = timeout;
375        self
376    }
377
378    /// Build and validate the configuration.
379    ///
380    /// # Errors
381    ///
382    /// - Returns error if `max_pool_size` is 0
383    /// - Returns error if `warmup_count` > `max_pool_size`
384    ///
385    /// # Example
386    ///
387    /// ```rust
388    /// use html2pdf_api::BrowserPoolConfigBuilder;
389    ///
390    /// // Valid configuration
391    /// let config = BrowserPoolConfigBuilder::new()
392    ///     .max_pool_size(10)
393    ///     .warmup_count(5)
394    ///     .build();
395    /// assert!(config.is_ok());
396    ///
397    /// // Invalid: pool size is 0
398    /// let config = BrowserPoolConfigBuilder::new()
399    ///     .max_pool_size(0)
400    ///     .build();
401    /// assert!(config.is_err());
402    ///
403    /// // Invalid: warmup exceeds pool size
404    /// let config = BrowserPoolConfigBuilder::new()
405    ///     .max_pool_size(5)
406    ///     .warmup_count(10)
407    ///     .build();
408    /// assert!(config.is_err());
409    /// ```
410    pub fn build(self) -> std::result::Result<BrowserPoolConfig, String> {
411        // Validation: Pool size must be positive
412        if self.config.max_pool_size == 0 {
413            return Err("max_pool_size must be greater than 0".to_string());
414        }
415
416        // Validation: Can't warmup more browsers than pool can hold
417        if self.config.warmup_count > self.config.max_pool_size {
418            return Err("warmup_count cannot exceed max_pool_size".to_string());
419        }
420
421        Ok(self.config)
422    }
423}
424
425impl Default for BrowserPoolConfigBuilder {
426    fn default() -> Self {
427        Self::new()
428    }
429}
430
431// ============================================================================
432// Environment Configuration (feature-gated)
433// ============================================================================
434
435/// Environment-based configuration loading.
436///
437/// This module is only available when the `env-config` feature is enabled.
438///
439/// # Environment File
440///
441/// This module uses `dotenvy` to load environment variables from an `app.env`
442/// file in the current directory. The file is optional - if not found,
443/// environment variables and defaults are used.
444///
445/// # Environment Variables
446///
447/// | Variable | Type | Default | Description |
448/// |----------|------|---------|-------------|
449/// | `BROWSER_POOL_SIZE` | usize | 5 | Maximum pool size |
450/// | `BROWSER_WARMUP_COUNT` | usize | 3 | Warmup browser count |
451/// | `BROWSER_TTL_SECONDS` | u64 | 3600 | Browser TTL in seconds |
452/// | `BROWSER_WARMUP_TIMEOUT_SECONDS` | u64 | 60 | Warmup timeout |
453/// | `BROWSER_PING_INTERVAL_SECONDS` | u64 | 15 | Health check interval |
454/// | `BROWSER_MAX_PING_FAILURES` | u32 | 3 | Max ping failures |
455/// | `CHROME_PATH` | String | auto | Custom Chrome binary path |
456///
457/// # Example `app.env` File
458///
459/// ```text
460/// # Browser Pool Configuration
461/// BROWSER_POOL_SIZE=5
462/// BROWSER_WARMUP_COUNT=3
463/// BROWSER_TTL_SECONDS=3600
464/// BROWSER_WARMUP_TIMEOUT_SECONDS=60
465/// BROWSER_PING_INTERVAL_SECONDS=15
466/// BROWSER_MAX_PING_FAILURES=3
467///
468/// # Chrome Configuration (optional)
469/// # CHROME_PATH=/usr/bin/google-chrome
470/// ```
471#[cfg(feature = "env-config")]
472pub mod env {
473    use super::*;
474    use crate::error::BrowserPoolError;
475
476    /// Default environment file name.
477    pub const ENV_FILE_NAME: &str = "app.env";
478
479    /// Load environment variables from `app.env` file.
480    ///
481    /// Call this early in your application startup to ensure environment
482    /// variables are loaded before any configuration functions are called.
483    ///
484    /// This function is automatically called by [`from_env`], but you can
485    /// call it explicitly if you need to load the file earlier or check
486    /// for errors.
487    ///
488    /// # Returns
489    ///
490    /// - `Ok(PathBuf)` if the file was found and loaded successfully
491    /// - `Err(dotenvy::Error)` if the file was not found or couldn't be parsed
492    ///
493    /// # Example
494    ///
495    /// ```rust,ignore
496    /// use html2pdf_api::config::env::load_env_file;
497    ///
498    /// // Load at application startup
499    /// match load_env_file() {
500    ///     Ok(path) => println!("Loaded environment from: {:?}", path),
501    ///     Err(e) => println!("No app.env file found: {}", e),
502    /// }
503    /// ```
504    pub fn load_env_file() -> Result<std::path::PathBuf, dotenvy::Error> {
505        dotenvy::from_filename(ENV_FILE_NAME)
506    }
507
508    /// Load configuration from environment variables.
509    ///
510    /// Reads configuration from environment variables with sensible defaults.
511    /// Also loads `app.env` file if present (via `dotenvy`).
512    ///
513    /// # Environment File
514    ///
515    /// This function looks for an `app.env` file in the current directory
516    /// and loads it if present. The file is optional - if not found,
517    /// environment variables and defaults are used.
518    ///
519    /// # Environment Variables
520    ///
521    /// - `BROWSER_POOL_SIZE`: Maximum pool size (default: 5)
522    /// - `BROWSER_WARMUP_COUNT`: Warmup browser count (default: 3)
523    /// - `BROWSER_TTL_SECONDS`: Browser TTL in seconds (default: 3600)
524    /// - `BROWSER_WARMUP_TIMEOUT_SECONDS`: Warmup timeout (default: 60)
525    /// - `BROWSER_PING_INTERVAL_SECONDS`: Health check interval (default: 15)
526    /// - `BROWSER_MAX_PING_FAILURES`: Max ping failures (default: 3)
527    ///
528    /// # Errors
529    ///
530    /// Returns [`BrowserPoolError::Configuration`] if configuration values are invalid.
531    ///
532    /// # Example
533    ///
534    /// ```rust,ignore
535    /// use html2pdf_api::config::env::from_env;
536    ///
537    /// // Set environment variables before calling
538    /// std::env::set_var("BROWSER_POOL_SIZE", "10");
539    ///
540    /// let config = from_env()?;
541    /// assert_eq!(config.max_pool_size, 10);
542    /// ```
543    pub fn from_env() -> Result<BrowserPoolConfig, BrowserPoolError> {
544        // Load app.env file if present (ignore errors if not found)
545        match load_env_file() {
546            Ok(path) => {
547                log::info!("� Loaded configuration from: {:?}", path);
548            }
549            Err(e) => {
550                log::debug!(
551                    "� No {} file found or failed to load: {} (using environment variables and defaults)",
552                    ENV_FILE_NAME,
553                    e
554                );
555            }
556        }
557
558        let max_pool_size = std::env::var("BROWSER_POOL_SIZE")
559            .ok()
560            .and_then(|s| s.parse().ok())
561            .unwrap_or(5);
562
563        let warmup_count = std::env::var("BROWSER_WARMUP_COUNT")
564            .ok()
565            .and_then(|s| s.parse().ok())
566            .unwrap_or(3);
567
568        let ttl_seconds = std::env::var("BROWSER_TTL_SECONDS")
569            .ok()
570            .and_then(|s| s.parse().ok())
571            .unwrap_or(3600u64);
572
573        let warmup_timeout_seconds = std::env::var("BROWSER_WARMUP_TIMEOUT_SECONDS")
574            .ok()
575            .and_then(|s| s.parse().ok())
576            .unwrap_or(60u64);
577
578        let ping_interval_seconds = std::env::var("BROWSER_PING_INTERVAL_SECONDS")
579            .ok()
580            .and_then(|s| s.parse().ok())
581            .unwrap_or(15u64);
582
583        let max_ping_failures = std::env::var("BROWSER_MAX_PING_FAILURES")
584            .ok()
585            .and_then(|s| s.parse().ok())
586            .unwrap_or(3);
587
588        log::info!("' Loading pool configuration from environment:");
589        log::info!("   - Max pool size: {}", max_pool_size);
590        log::info!("   - Warmup count: {}", warmup_count);
591        log::info!(
592            "   - Browser TTL: {}s ({}min)",
593            ttl_seconds,
594            ttl_seconds / 60
595        );
596        log::info!("   - Warmup timeout: {}s", warmup_timeout_seconds);
597        log::info!("   - Ping interval: {}s", ping_interval_seconds);
598        log::info!("   - Max ping failures: {}", max_ping_failures);
599
600        BrowserPoolConfigBuilder::new()
601            .max_pool_size(max_pool_size)
602            .warmup_count(warmup_count)
603            .browser_ttl(Duration::from_secs(ttl_seconds))
604            .warmup_timeout(Duration::from_secs(warmup_timeout_seconds))
605            .ping_interval(Duration::from_secs(ping_interval_seconds))
606            .max_ping_failures(max_ping_failures)
607            .build()
608            .map_err(BrowserPoolError::Configuration)
609    }
610
611    /// Get Chrome path from environment.
612    ///
613    /// Reads `CHROME_PATH` environment variable.
614    ///
615    /// **Note:** Call [`from_env`] or [`load_env_file`] first to ensure
616    /// `app.env` is loaded if you're using a configuration file.
617    ///
618    /// # Returns
619    ///
620    /// - `Some(path)` if `CHROME_PATH` is set
621    /// - `None` if not set (will use auto-detection)
622    ///
623    /// # Example
624    ///
625    /// ```rust,ignore
626    /// use html2pdf_api::config::env::{load_env_file, chrome_path_from_env};
627    ///
628    /// // Ensure app.env is loaded first
629    /// let _ = load_env_file();
630    ///
631    /// let path = chrome_path_from_env();
632    /// if let Some(p) = path {
633    ///     println!("Using Chrome at: {}", p);
634    /// }
635    /// ```
636    pub fn chrome_path_from_env() -> Option<String> {
637        std::env::var("CHROME_PATH").ok()
638    }
639}
640
641// ============================================================================
642// Unit Tests
643// ============================================================================
644
645#[cfg(test)]
646mod tests {
647    use super::*;
648
649    /// Verifies that BrowserPoolConfigBuilder correctly sets all configuration values.
650    ///
651    /// Tests the happy path where all values are valid and within constraints.
652    #[test]
653    fn test_config_builder() {
654        let config = BrowserPoolConfigBuilder::new()
655            .max_pool_size(10)
656            .warmup_count(5)
657            .browser_ttl(Duration::from_secs(7200))
658            .warmup_timeout(Duration::from_secs(120))
659            .build()
660            .unwrap();
661
662        assert_eq!(config.max_pool_size, 10);
663        assert_eq!(config.warmup_count, 5);
664        assert_eq!(config.browser_ttl.as_secs(), 7200);
665        assert_eq!(config.warmup_timeout.as_secs(), 120);
666    }
667
668    /// Verifies that config builder rejects invalid pool size (zero).
669    ///
670    /// Pool size must be at least 1 to be useful. This test ensures
671    /// the validation catches this error at build time.
672    #[test]
673    fn test_config_validation() {
674        let result = BrowserPoolConfigBuilder::new().max_pool_size(0).build();
675
676        assert!(result.is_err());
677        let err_msg = result.unwrap_err();
678        assert!(
679            err_msg.contains("max_pool_size must be greater than 0"),
680            "Expected validation error message, got: {}",
681            err_msg
682        );
683    }
684
685    /// Verifies that warmup count cannot exceed pool size.
686    ///
687    /// It's illogical to warmup more browsers than the pool can hold.
688    /// This test ensures the configuration builder catches this mistake.
689    #[test]
690    fn test_config_warmup_exceeds_pool() {
691        let result = BrowserPoolConfigBuilder::new()
692            .max_pool_size(5)
693            .warmup_count(10)
694            .build();
695
696        assert!(result.is_err());
697        let err_msg = result.unwrap_err();
698        assert!(
699            err_msg.contains("warmup_count cannot exceed max_pool_size"),
700            "Expected validation error message, got: {}",
701            err_msg
702        );
703    }
704
705    /// Verifies that default configuration values are production-ready.
706    ///
707    /// These defaults are used when no explicit configuration is provided.
708    /// They should be safe and reasonable for most use cases.
709    #[test]
710    fn test_config_defaults() {
711        let config = BrowserPoolConfig::default();
712
713        // Verify production-ready defaults
714        assert_eq!(config.max_pool_size, 5, "Default pool size should be 5");
715        assert_eq!(config.warmup_count, 3, "Default warmup should be 3");
716        assert_eq!(
717            config.ping_interval,
718            Duration::from_secs(15),
719            "Default ping interval should be 15s"
720        );
721        assert_eq!(
722            config.browser_ttl,
723            Duration::from_secs(3600),
724            "Default TTL should be 1 hour"
725        );
726        assert_eq!(
727            config.max_ping_failures, 3,
728            "Default max failures should be 3"
729        );
730        assert_eq!(
731            config.warmup_timeout,
732            Duration::from_secs(60),
733            "Default warmup timeout should be 60s"
734        );
735    }
736
737    /// Verifies that config builder supports method chaining.
738    ///
739    /// The builder pattern should allow fluent API usage where all
740    /// setters can be chained together.
741    #[test]
742    fn test_config_builder_chaining() {
743        let config = BrowserPoolConfigBuilder::new()
744            .max_pool_size(8)
745            .warmup_count(4)
746            .ping_interval(Duration::from_secs(30))
747            .browser_ttl(Duration::from_secs(1800))
748            .max_ping_failures(5)
749            .warmup_timeout(Duration::from_secs(90))
750            .build()
751            .unwrap();
752
753        // Verify all chained values were set correctly
754        assert_eq!(config.max_pool_size, 8);
755        assert_eq!(config.warmup_count, 4);
756        assert_eq!(config.ping_interval.as_secs(), 30);
757        assert_eq!(config.browser_ttl.as_secs(), 1800);
758        assert_eq!(config.max_ping_failures, 5);
759        assert_eq!(config.warmup_timeout.as_secs(), 90);
760    }
761
762    /// Verifies that BrowserPoolConfigBuilder implements Default.
763    #[test]
764    fn test_builder_default() {
765        let builder: BrowserPoolConfigBuilder = Default::default();
766        let config = builder.build().unwrap();
767
768        // Should have same values as BrowserPoolConfig::default()
769        assert_eq!(config.max_pool_size, 5);
770        assert_eq!(config.warmup_count, 3);
771    }
772}