Skip to main content

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    /// Interval between pre-created browser startups during warmup.
151    ///
152    /// Distributes expiration intervals so the entire pool doesn't crash simultaneously.
153    /// Default: 30 seconds
154    pub warmup_stagger: Duration,
155}
156
157impl Default for BrowserPoolConfig {
158    /// Production-ready default configuration.
159    ///
160    /// - Pool size: 5 browsers
161    /// - Warmup: 3 browsers
162    /// - Health checks: Every 15 seconds
163    /// - TTL: 1 hour
164    /// - Failure tolerance: 3 consecutive failures
165    /// - Warmup timeout: 60 seconds
166    ///
167    /// # Example
168    ///
169    /// ```rust
170    /// use html2pdf_api::BrowserPoolConfig;
171    /// use std::time::Duration;
172    ///
173    /// let config = BrowserPoolConfig::default();
174    ///
175    /// assert_eq!(config.max_pool_size, 5);
176    /// assert_eq!(config.warmup_count, 3);
177    /// assert_eq!(config.ping_interval, Duration::from_secs(15));
178    /// assert_eq!(config.browser_ttl, Duration::from_secs(3600));
179    /// assert_eq!(config.max_ping_failures, 3);
180    /// assert_eq!(config.warmup_timeout, Duration::from_secs(60));
181    /// ```
182    fn default() -> Self {
183        Self {
184            max_pool_size: 5,
185            warmup_count: 3,
186            ping_interval: Duration::from_secs(15),
187            browser_ttl: Duration::from_secs(3600), // 1 hour
188            max_ping_failures: 3,
189            warmup_timeout: Duration::from_secs(60),
190            warmup_stagger: Duration::from_secs(30),
191        }
192    }
193}
194
195/// Builder for [`BrowserPoolConfig`] with validation.
196///
197/// Provides a fluent API for constructing validated configurations.
198/// All setter methods can be chained together.
199///
200/// # Example
201///
202/// ```rust
203/// use std::time::Duration;
204/// use html2pdf_api::BrowserPoolConfigBuilder;
205///
206/// let config = BrowserPoolConfigBuilder::new()
207///     .max_pool_size(10)
208///     .warmup_count(5)
209///     .browser_ttl(Duration::from_secs(7200))
210///     .build()
211///     .expect("Invalid configuration");
212/// ```
213///
214/// # Validation
215///
216/// The [`build()`](Self::build) method validates:
217/// - `max_pool_size` must be greater than 0
218/// - `warmup_count` must be d `max_pool_size`
219pub struct BrowserPoolConfigBuilder {
220    config: BrowserPoolConfig,
221}
222
223impl BrowserPoolConfigBuilder {
224    /// Create a new builder with default values.
225    ///
226    /// # Example
227    ///
228    /// ```rust
229    /// use html2pdf_api::BrowserPoolConfigBuilder;
230    ///
231    /// let builder = BrowserPoolConfigBuilder::new();
232    /// let config = builder.build().unwrap();
233    ///
234    /// // Has default values
235    /// assert_eq!(config.max_pool_size, 5);
236    /// ```
237    pub fn new() -> Self {
238        Self {
239            config: BrowserPoolConfig::default(),
240        }
241    }
242
243    /// Set maximum pool size (must be > 0).
244    ///
245    /// # Parameters
246    ///
247    /// * `size` - Maximum number of browsers in the pool.
248    ///
249    /// # Example
250    ///
251    /// ```rust
252    /// use html2pdf_api::BrowserPoolConfigBuilder;
253    ///
254    /// let config = BrowserPoolConfigBuilder::new()
255    ///     .max_pool_size(10)
256    ///     .build()
257    ///     .unwrap();
258    ///
259    /// assert_eq!(config.max_pool_size, 10);
260    /// ```
261    pub fn max_pool_size(mut self, size: usize) -> Self {
262        self.config.max_pool_size = size;
263        self
264    }
265
266    /// Set warmup count (must be d max_pool_size).
267    ///
268    /// # Parameters
269    ///
270    /// * `count` - Number of browsers to pre-create during warmup.
271    ///
272    /// # Example
273    ///
274    /// ```rust
275    /// use html2pdf_api::BrowserPoolConfigBuilder;
276    ///
277    /// let config = BrowserPoolConfigBuilder::new()
278    ///     .max_pool_size(10)
279    ///     .warmup_count(5)
280    ///     .build()
281    ///     .unwrap();
282    ///
283    /// assert_eq!(config.warmup_count, 5);
284    /// ```
285    pub fn warmup_count(mut self, count: usize) -> Self {
286        self.config.warmup_count = count;
287        self
288    }
289
290    /// Set health check interval.
291    ///
292    /// # Parameters
293    ///
294    /// * `interval` - Duration between health check pings.
295    ///
296    /// # Example
297    ///
298    /// ```rust
299    /// use std::time::Duration;
300    /// use html2pdf_api::BrowserPoolConfigBuilder;
301    ///
302    /// let config = BrowserPoolConfigBuilder::new()
303    ///     .ping_interval(Duration::from_secs(30))
304    ///     .build()
305    ///     .unwrap();
306    ///
307    /// assert_eq!(config.ping_interval, Duration::from_secs(30));
308    /// ```
309    pub fn ping_interval(mut self, interval: Duration) -> Self {
310        self.config.ping_interval = interval;
311        self
312    }
313
314    /// Set browser time-to-live before forced retirement.
315    ///
316    /// # Parameters
317    ///
318    /// * `ttl` - Maximum lifetime for each browser instance.
319    ///
320    /// # Example
321    ///
322    /// ```rust
323    /// use std::time::Duration;
324    /// use html2pdf_api::BrowserPoolConfigBuilder;
325    ///
326    /// let config = BrowserPoolConfigBuilder::new()
327    ///     .browser_ttl(Duration::from_secs(7200)) // 2 hours
328    ///     .build()
329    ///     .unwrap();
330    ///
331    /// assert_eq!(config.browser_ttl, Duration::from_secs(7200));
332    /// ```
333    pub fn browser_ttl(mut self, ttl: Duration) -> Self {
334        self.config.browser_ttl = ttl;
335        self
336    }
337
338    /// Set maximum consecutive ping failures before removal.
339    ///
340    /// # Parameters
341    ///
342    /// * `failures` - Number of consecutive failures tolerated.
343    ///
344    /// # Example
345    ///
346    /// ```rust
347    /// use html2pdf_api::BrowserPoolConfigBuilder;
348    ///
349    /// let config = BrowserPoolConfigBuilder::new()
350    ///     .max_ping_failures(5)
351    ///     .build()
352    ///     .unwrap();
353    ///
354    /// assert_eq!(config.max_ping_failures, 5);
355    /// ```
356    pub fn max_ping_failures(mut self, failures: u32) -> Self {
357        self.config.max_ping_failures = failures;
358        self
359    }
360
361    /// Set warmup timeout.
362    ///
363    /// # Parameters
364    ///
365    /// * `timeout` - Maximum time allowed for warmup to complete.
366    ///
367    /// # Example
368    ///
369    /// ```rust
370    /// use std::time::Duration;
371    /// use html2pdf_api::BrowserPoolConfigBuilder;
372    ///
373    /// let config = BrowserPoolConfigBuilder::new()
374    ///     .warmup_timeout(Duration::from_secs(120))
375    ///     .build()
376    ///     .unwrap();
377    ///
378    /// assert_eq!(config.warmup_timeout, Duration::from_secs(120));
379    /// ```
380    pub fn warmup_timeout(mut self, timeout: Duration) -> Self {
381        self.config.warmup_timeout = timeout;
382        self
383    }
384
385    /// Set warmup stagger interval.
386    ///
387    /// # Parameters
388    ///
389    /// * `stagger` - Delay between browser spawns during initial warmup.
390    pub fn warmup_stagger(mut self, stagger: Duration) -> Self {
391        self.config.warmup_stagger = stagger;
392        self
393    }
394
395    /// Build and validate the configuration.
396    ///
397    /// # Errors
398    ///
399    /// - Returns error if `max_pool_size` is 0
400    /// - Returns error if `warmup_count` > `max_pool_size`
401    ///
402    /// # Example
403    ///
404    /// ```rust
405    /// use html2pdf_api::BrowserPoolConfigBuilder;
406    ///
407    /// // Valid configuration
408    /// let config = BrowserPoolConfigBuilder::new()
409    ///     .max_pool_size(10)
410    ///     .warmup_count(5)
411    ///     .build();
412    /// assert!(config.is_ok());
413    ///
414    /// // Invalid: pool size is 0
415    /// let config = BrowserPoolConfigBuilder::new()
416    ///     .max_pool_size(0)
417    ///     .build();
418    /// assert!(config.is_err());
419    ///
420    /// // Invalid: warmup exceeds pool size
421    /// let config = BrowserPoolConfigBuilder::new()
422    ///     .max_pool_size(5)
423    ///     .warmup_count(10)
424    ///     .build();
425    /// assert!(config.is_err());
426    /// ```
427    pub fn build(self) -> std::result::Result<BrowserPoolConfig, String> {
428        // Validation: Pool size must be positive
429        if self.config.max_pool_size == 0 {
430            return Err("max_pool_size must be greater than 0".to_string());
431        }
432
433        // Validation: Can't warmup more browsers than pool can hold
434        if self.config.warmup_count > self.config.max_pool_size {
435            return Err("warmup_count cannot exceed max_pool_size".to_string());
436        }
437
438        Ok(self.config)
439    }
440}
441
442impl Default for BrowserPoolConfigBuilder {
443    fn default() -> Self {
444        Self::new()
445    }
446}
447
448// ============================================================================
449// Environment Configuration (feature-gated)
450// ============================================================================
451
452/// Environment-based configuration loading.
453///
454/// This module is only available when the `env-config` feature is enabled.
455///
456/// # Environment File
457///
458/// This module uses `dotenvy` to load environment variables from an `app.env`
459/// file in the current directory. The file is optional - if not found,
460/// environment variables and defaults are used.
461///
462/// # Environment Variables
463///
464/// | Variable | Type | Default | Description |
465/// |----------|------|---------|-------------|
466/// | `BROWSER_POOL_SIZE` | usize | 5 | Maximum pool size |
467/// | `BROWSER_WARMUP_COUNT` | usize | 3 | Warmup browser count |
468/// | `BROWSER_TTL_SECONDS` | u64 | 3600 | Browser TTL in seconds |
469/// | `BROWSER_WARMUP_TIMEOUT_SECONDS` | u64 | 60 | Warmup timeout |
470/// | `BROWSER_PING_INTERVAL_SECONDS` | u64 | 15 | Health check interval |
471/// | `BROWSER_MAX_PING_FAILURES` | u32 | 3 | Max ping failures |
472/// | `CHROME_PATH` | String | auto | Custom Chrome binary path |
473///
474/// # Example `app.env` File
475///
476/// ```text
477/// # Browser Pool Configuration
478/// BROWSER_POOL_SIZE=5
479/// BROWSER_WARMUP_COUNT=3
480/// BROWSER_TTL_SECONDS=3600
481/// BROWSER_WARMUP_TIMEOUT_SECONDS=60
482/// BROWSER_PING_INTERVAL_SECONDS=15
483/// BROWSER_MAX_PING_FAILURES=3
484///
485/// # Chrome Configuration (optional)
486/// # CHROME_PATH=/usr/bin/google-chrome
487/// ```
488#[cfg(feature = "env-config")]
489pub mod env {
490    use super::*;
491    use crate::error::BrowserPoolError;
492
493    /// Default environment file name.
494    pub const ENV_FILE_NAME: &str = "app.env";
495
496    /// Load environment variables from `app.env` file.
497    ///
498    /// Call this early in your application startup to ensure environment
499    /// variables are loaded before any configuration functions are called.
500    ///
501    /// This function is automatically called by [`from_env`], but you can
502    /// call it explicitly if you need to load the file earlier or check
503    /// for errors.
504    ///
505    /// # Returns
506    ///
507    /// - `Ok(PathBuf)` if the file was found and loaded successfully
508    /// - `Err(dotenvy::Error)` if the file was not found or couldn't be parsed
509    ///
510    /// # Example
511    ///
512    /// ```rust,ignore
513    /// use html2pdf_api::config::env::load_env_file;
514    ///
515    /// // Load at application startup
516    /// match load_env_file() {
517    ///     Ok(path) => println!("Loaded environment from: {:?}", path),
518    ///     Err(e) => println!("No app.env file found: {}", e),
519    /// }
520    /// ```
521    pub fn load_env_file() -> Result<std::path::PathBuf, dotenvy::Error> {
522        dotenvy::from_filename(ENV_FILE_NAME)
523    }
524
525    /// Load configuration from environment variables.
526    ///
527    /// Reads configuration from environment variables with sensible defaults.
528    /// Also loads `app.env` file if present (via `dotenvy`).
529    ///
530    /// # Environment File
531    ///
532    /// This function looks for an `app.env` file in the current directory
533    /// and loads it if present. The file is optional - if not found,
534    /// environment variables and defaults are used.
535    ///
536    /// # Environment Variables
537    ///
538    /// - `BROWSER_POOL_SIZE`: Maximum pool size (default: 5)
539    /// - `BROWSER_WARMUP_COUNT`: Warmup browser count (default: 3)
540    /// - `BROWSER_TTL_SECONDS`: Browser TTL in seconds (default: 3600)
541    /// - `BROWSER_WARMUP_TIMEOUT_SECONDS`: Warmup timeout (default: 60)
542    /// - `BROWSER_PING_INTERVAL_SECONDS`: Health check interval (default: 15)
543    /// - `BROWSER_MAX_PING_FAILURES`: Max ping failures (default: 3)
544    ///
545    /// # Errors
546    ///
547    /// Returns [`BrowserPoolError::Configuration`] if configuration values are invalid.
548    ///
549    /// # Example
550    ///
551    /// ```rust,ignore
552    /// use html2pdf_api::config::env::from_env;
553    ///
554    /// // Set environment variables before calling
555    /// std::env::set_var("BROWSER_POOL_SIZE", "10");
556    ///
557    /// let config = from_env()?;
558    /// assert_eq!(config.max_pool_size, 10);
559    /// ```
560    pub fn from_env() -> Result<BrowserPoolConfig, BrowserPoolError> {
561        // Load app.env file if present (ignore errors if not found)
562        match load_env_file() {
563            Ok(path) => {
564                log::info!("⚙️ Loaded configuration from: {:?}", path);
565            }
566            Err(e) => {
567                log::debug!(
568                    "⚠️ No {} file found or failed to load: {} (using environment variables and defaults)",
569                    ENV_FILE_NAME,
570                    e
571                );
572            }
573        }
574
575        let max_pool_size = std::env::var("BROWSER_POOL_SIZE")
576            .ok()
577            .and_then(|s| s.parse().ok())
578            .unwrap_or(5);
579
580        let warmup_count = std::env::var("BROWSER_WARMUP_COUNT")
581            .ok()
582            .and_then(|s| s.parse().ok())
583            .unwrap_or(3);
584
585        let ttl_seconds = std::env::var("BROWSER_TTL_SECONDS")
586            .ok()
587            .and_then(|s| s.parse().ok())
588            .unwrap_or(3600u64);
589
590        let warmup_timeout_seconds = std::env::var("BROWSER_WARMUP_TIMEOUT_SECONDS")
591            .ok()
592            .and_then(|s| s.parse().ok())
593            .unwrap_or(60u64);
594
595        let ping_interval_seconds = std::env::var("BROWSER_PING_INTERVAL_SECONDS")
596            .ok()
597            .and_then(|s| s.parse().ok())
598            .unwrap_or(15u64);
599
600        let max_ping_failures = std::env::var("BROWSER_MAX_PING_FAILURES")
601            .ok()
602            .and_then(|s| s.parse().ok())
603            .unwrap_or(3);
604
605        log::info!("' Loading pool configuration from environment:");
606        log::info!("   - Max pool size: {}", max_pool_size);
607        log::info!("   - Warmup count: {}", warmup_count);
608        log::info!(
609            "   - Browser TTL: {}s ({}min)",
610            ttl_seconds,
611            ttl_seconds / 60
612        );
613        log::info!("   - Warmup timeout: {}s", warmup_timeout_seconds);
614        log::info!("   - Ping interval: {}s", ping_interval_seconds);
615        log::info!("   - Max ping failures: {}", max_ping_failures);
616
617        BrowserPoolConfigBuilder::new()
618            .max_pool_size(max_pool_size)
619            .warmup_count(warmup_count)
620            .browser_ttl(Duration::from_secs(ttl_seconds))
621            .warmup_timeout(Duration::from_secs(warmup_timeout_seconds))
622            .ping_interval(Duration::from_secs(ping_interval_seconds))
623            .max_ping_failures(max_ping_failures)
624            .build()
625            .map_err(BrowserPoolError::Configuration)
626    }
627
628    /// Get Chrome path from environment.
629    ///
630    /// Reads `CHROME_PATH` environment variable.
631    ///
632    /// **Note:** Call [`from_env`] or [`load_env_file`] first to ensure
633    /// `app.env` is loaded if you're using a configuration file.
634    ///
635    /// # Returns
636    ///
637    /// - `Some(path)` if `CHROME_PATH` is set
638    /// - `None` if not set (will use auto-detection)
639    ///
640    /// # Example
641    ///
642    /// ```rust,ignore
643    /// use html2pdf_api::config::env::{load_env_file, chrome_path_from_env};
644    ///
645    /// // Ensure app.env is loaded first
646    /// let _ = load_env_file();
647    ///
648    /// let path = chrome_path_from_env();
649    /// if let Some(p) = path {
650    ///     println!("Using Chrome at: {}", p);
651    /// }
652    /// ```
653    pub fn chrome_path_from_env() -> Option<String> {
654        std::env::var("CHROME_PATH").ok()
655    }
656}
657
658// ============================================================================
659// Unit Tests
660// ============================================================================
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665
666    /// Verifies that BrowserPoolConfigBuilder correctly sets all configuration values.
667    ///
668    /// Tests the happy path where all values are valid and within constraints.
669    #[test]
670    fn test_config_builder() {
671        let config = BrowserPoolConfigBuilder::new()
672            .max_pool_size(10)
673            .warmup_count(5)
674            .browser_ttl(Duration::from_secs(7200))
675            .warmup_timeout(Duration::from_secs(120))
676            .build()
677            .unwrap();
678
679        assert_eq!(config.max_pool_size, 10);
680        assert_eq!(config.warmup_count, 5);
681        assert_eq!(config.browser_ttl.as_secs(), 7200);
682        assert_eq!(config.warmup_timeout.as_secs(), 120);
683    }
684
685    /// Verifies that config builder rejects invalid pool size (zero).
686    ///
687    /// Pool size must be at least 1 to be useful. This test ensures
688    /// the validation catches this error at build time.
689    #[test]
690    fn test_config_validation() {
691        let result = BrowserPoolConfigBuilder::new().max_pool_size(0).build();
692
693        assert!(result.is_err());
694        let err_msg = result.unwrap_err();
695        assert!(
696            err_msg.contains("max_pool_size must be greater than 0"),
697            "Expected validation error message, got: {}",
698            err_msg
699        );
700    }
701
702    /// Verifies that warmup count cannot exceed pool size.
703    ///
704    /// It's illogical to warmup more browsers than the pool can hold.
705    /// This test ensures the configuration builder catches this mistake.
706    #[test]
707    fn test_config_warmup_exceeds_pool() {
708        let result = BrowserPoolConfigBuilder::new()
709            .max_pool_size(5)
710            .warmup_count(10)
711            .build();
712
713        assert!(result.is_err());
714        let err_msg = result.unwrap_err();
715        assert!(
716            err_msg.contains("warmup_count cannot exceed max_pool_size"),
717            "Expected validation error message, got: {}",
718            err_msg
719        );
720    }
721
722    /// Verifies that default configuration values are production-ready.
723    ///
724    /// These defaults are used when no explicit configuration is provided.
725    /// They should be safe and reasonable for most use cases.
726    #[test]
727    fn test_config_defaults() {
728        let config = BrowserPoolConfig::default();
729
730        // Verify production-ready defaults
731        assert_eq!(config.max_pool_size, 5, "Default pool size should be 5");
732        assert_eq!(config.warmup_count, 3, "Default warmup should be 3");
733        assert_eq!(
734            config.ping_interval,
735            Duration::from_secs(15),
736            "Default ping interval should be 15s"
737        );
738        assert_eq!(
739            config.browser_ttl,
740            Duration::from_secs(3600),
741            "Default TTL should be 1 hour"
742        );
743        assert_eq!(
744            config.max_ping_failures, 3,
745            "Default max failures should be 3"
746        );
747        assert_eq!(
748            config.warmup_timeout,
749            Duration::from_secs(60),
750            "Default warmup timeout should be 60s"
751        );
752    }
753
754    /// Verifies that config builder supports method chaining.
755    ///
756    /// The builder pattern should allow fluent API usage where all
757    /// setters can be chained together.
758    #[test]
759    fn test_config_builder_chaining() {
760        let config = BrowserPoolConfigBuilder::new()
761            .max_pool_size(8)
762            .warmup_count(4)
763            .ping_interval(Duration::from_secs(30))
764            .browser_ttl(Duration::from_secs(1800))
765            .max_ping_failures(5)
766            .warmup_timeout(Duration::from_secs(90))
767            .build()
768            .unwrap();
769
770        // Verify all chained values were set correctly
771        assert_eq!(config.max_pool_size, 8);
772        assert_eq!(config.warmup_count, 4);
773        assert_eq!(config.ping_interval.as_secs(), 30);
774        assert_eq!(config.browser_ttl.as_secs(), 1800);
775        assert_eq!(config.max_ping_failures, 5);
776        assert_eq!(config.warmup_timeout.as_secs(), 90);
777    }
778
779    /// Verifies that BrowserPoolConfigBuilder implements Default.
780    #[test]
781    fn test_builder_default() {
782        let builder: BrowserPoolConfigBuilder = Default::default();
783        let config = builder.build().unwrap();
784
785        // Should have same values as BrowserPoolConfig::default()
786        assert_eq!(config.max_pool_size, 5);
787        assert_eq!(config.warmup_count, 3);
788    }
789}