Skip to main content

chrome_for_testing_manager/
mgr.rs

1use crate::cache::CacheDir;
2use crate::download;
3use crate::output::{DriverOutputInspectors, DriverOutputListener};
4use crate::port::{Port, PortRequest};
5use crate::version::{SelectedVersion, VersionRequest};
6use crate::{ChromeForTestingArtifact, ChromeForTestingManagerError};
7use chrome_for_testing::{KnownGoodVersions, LastKnownGoodVersions, Platform, Version};
8use rootcause::{Report, bail, option_ext::OptionExt, prelude::ResultExt, report};
9#[cfg(feature = "thirtyfour")]
10use std::collections::VecDeque;
11use std::path::{Path, PathBuf};
12use std::sync::Arc;
13#[cfg(feature = "thirtyfour")]
14use std::sync::Mutex;
15use std::sync::atomic::AtomicU16;
16use std::time::Duration;
17use tokio::fs;
18use tokio::process::Command;
19use tokio_process_tools::{
20    BroadcastOutputStream, DEFAULT_MAX_BUFFERED_CHUNKS, DEFAULT_MAX_LINE_LENGTH,
21    DEFAULT_READ_CHUNK_SIZE, GracefulShutdown, LineOverflowBehavior, LineParsingOptions,
22    NumBytesExt, Process, ProcessHandle, ReliableWithBackpressure, ReplayEnabled,
23    WaitForLineResult,
24};
25
26type ManagedProcessOutput = BroadcastOutputStream<ReliableWithBackpressure, ReplayEnabled>;
27type ManagedProcessHandle = ProcessHandle<ManagedProcessOutput>;
28#[cfg(feature = "thirtyfour")]
29type RecentBrowserOutput = Arc<Mutex<VecDeque<String>>>;
30
31#[cfg(feature = "thirtyfour")]
32const BROWSER_STARTUP_OUTPUT_LINES: usize = 80;
33#[cfg(feature = "thirtyfour")]
34const DEFAULT_HEADLESS_SHELL_REMOTE_DEBUGGING_ARG: &str = "--remote-debugging-port=0";
35
36/// Chrome-compatible browser binary to register with `ChromeDriver`.
37#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
38#[non_exhaustive]
39pub enum ChromeBinary {
40    /// The regular Chrome for Testing browser package.
41    #[default]
42    Chrome,
43
44    /// The Chrome Headless Shell package.
45    ChromeHeadlessShell,
46}
47
48impl ChromeBinary {
49    const fn label(self) -> &'static str {
50        match self {
51            Self::Chrome => "Chrome",
52            Self::ChromeHeadlessShell => "Chrome Headless Shell",
53        }
54    }
55
56    const fn artifact(self) -> ChromeForTestingArtifact {
57        match self {
58            Self::Chrome => ChromeForTestingArtifact::Chrome,
59            Self::ChromeHeadlessShell => ChromeForTestingArtifact::ChromeHeadlessShell,
60        }
61    }
62
63    fn executable_path(self, platform: Platform) -> &'static Path {
64        match self {
65            Self::Chrome => platform.chrome_executable_path(),
66            Self::ChromeHeadlessShell => platform.chrome_headless_shell_executable_path(),
67        }
68    }
69}
70
71#[cfg(feature = "thirtyfour")]
72#[derive(Debug)]
73pub(crate) struct HeadlessShellSession {
74    process: tokio_process_tools::TerminateOnDrop<ManagedProcessOutput>,
75    debugger_address: String,
76    shutdown: GracefulShutdown,
77}
78
79#[cfg(feature = "thirtyfour")]
80impl HeadlessShellSession {
81    pub(crate) fn debugger_address(&self) -> &str {
82        &self.debugger_address
83    }
84
85    pub(crate) async fn terminate(
86        mut self,
87    ) -> Result<std::process::ExitStatus, Report<ChromeForTestingManagerError>> {
88        self.process.terminate(self.shutdown.clone()).await.context(
89            ChromeForTestingManagerError::TerminateBrowser {
90                debugger_address: self.debugger_address.clone(),
91            },
92        )
93    }
94}
95
96/// A downloaded regular Chrome for Testing package paired with a matching `ChromeDriver`.
97///
98/// Use this when code specifically needs the full Chrome browser package. APIs that can operate
99/// on either regular Chrome or Chrome Headless Shell use [`LoadedBrowserPackage`] instead.
100#[derive(Debug, Clone)]
101pub struct LoadedChromePackage {
102    chrome_executable: PathBuf,
103    chromedriver_executable: PathBuf,
104}
105
106impl LoadedChromePackage {
107    fn new(chrome_executable: PathBuf, chromedriver_executable: PathBuf) -> Self {
108        Self {
109            chrome_executable,
110            chromedriver_executable,
111        }
112    }
113
114    /// Path to the cached regular Chrome executable.
115    #[must_use]
116    pub fn chrome_executable(&self) -> &Path {
117        &self.chrome_executable
118    }
119
120    /// Path to the cached `ChromeDriver` executable.
121    #[must_use]
122    pub fn chromedriver_executable(&self) -> &Path {
123        &self.chromedriver_executable
124    }
125}
126
127/// A downloaded Chrome Headless Shell package paired with a matching `ChromeDriver`.
128///
129/// Use this when code specifically needs the headless-shell package. APIs that can operate on
130/// either regular Chrome or Chrome Headless Shell use [`LoadedBrowserPackage`] instead.
131#[derive(Debug, Clone)]
132pub struct LoadedChromeHeadlessShellPackage {
133    chrome_headless_shell_executable: PathBuf,
134    chromedriver_executable: PathBuf,
135}
136
137impl LoadedChromeHeadlessShellPackage {
138    fn new(chrome_headless_shell_executable: PathBuf, chromedriver_executable: PathBuf) -> Self {
139        Self {
140            chrome_headless_shell_executable,
141            chromedriver_executable,
142        }
143    }
144
145    /// Path to the cached Chrome Headless Shell executable.
146    #[must_use]
147    pub fn chrome_headless_shell_executable(&self) -> &Path {
148        &self.chrome_headless_shell_executable
149    }
150
151    /// Path to the cached `ChromeDriver` executable.
152    #[must_use]
153    pub fn chromedriver_executable(&self) -> &Path {
154        &self.chromedriver_executable
155    }
156}
157
158/// A downloaded Chrome-compatible browser package paired with a matching `ChromeDriver`.
159///
160/// Returned by [`ChromeForTestingManager::download`]. Match on the enum when behavior differs
161/// between regular Chrome and Chrome Headless Shell, or use [`Self::browser_executable`] and
162/// [`Self::chromedriver_executable`] for behavior shared by both browser packages.
163#[derive(Debug, Clone)]
164#[non_exhaustive]
165pub enum LoadedBrowserPackage {
166    /// Regular Chrome for Testing.
167    Chrome(LoadedChromePackage),
168
169    /// Chrome Headless Shell.
170    ChromeHeadlessShell(LoadedChromeHeadlessShellPackage),
171}
172
173impl LoadedBrowserPackage {
174    fn new(
175        chrome_binary: ChromeBinary,
176        browser_executable: PathBuf,
177        chromedriver_executable: PathBuf,
178    ) -> Self {
179        match chrome_binary {
180            ChromeBinary::Chrome => Self::Chrome(LoadedChromePackage::new(
181                browser_executable,
182                chromedriver_executable,
183            )),
184            ChromeBinary::ChromeHeadlessShell => Self::ChromeHeadlessShell(
185                LoadedChromeHeadlessShellPackage::new(browser_executable, chromedriver_executable),
186            ),
187        }
188    }
189
190    /// The Chrome-compatible browser binary selected for this package.
191    #[must_use]
192    pub const fn chrome_binary(&self) -> ChromeBinary {
193        match self {
194            Self::Chrome(_) => ChromeBinary::Chrome,
195            Self::ChromeHeadlessShell(_) => ChromeBinary::ChromeHeadlessShell,
196        }
197    }
198
199    /// Path to the cached browser executable.
200    #[must_use]
201    pub fn browser_executable(&self) -> &Path {
202        match self {
203            Self::Chrome(package) => package.chrome_executable(),
204            Self::ChromeHeadlessShell(package) => package.chrome_headless_shell_executable(),
205        }
206    }
207
208    /// Path to the cached `ChromeDriver` executable.
209    #[must_use]
210    pub fn chromedriver_executable(&self) -> &Path {
211        match self {
212            Self::Chrome(package) => package.chromedriver_executable(),
213            Self::ChromeHeadlessShell(package) => package.chromedriver_executable(),
214        }
215    }
216}
217
218#[derive(Debug, Clone, Copy)]
219struct RequestedChromeBinaries {
220    chrome: bool,
221    chrome_headless_shell: bool,
222}
223
224impl RequestedChromeBinaries {
225    const fn single(chrome_binary: ChromeBinary) -> Self {
226        match chrome_binary {
227            ChromeBinary::Chrome => Self {
228                chrome: true,
229                chrome_headless_shell: false,
230            },
231            ChromeBinary::ChromeHeadlessShell => Self {
232                chrome: false,
233                chrome_headless_shell: true,
234            },
235        }
236    }
237
238    fn from_slice(
239        chrome_binaries: &[ChromeBinary],
240    ) -> Result<Self, Report<ChromeForTestingManagerError>> {
241        if chrome_binaries.is_empty() {
242            bail!(ChromeForTestingManagerError::EmptyChromeBinaryDownloadRequest);
243        }
244
245        Ok(Self {
246            chrome: chrome_binaries.contains(&ChromeBinary::Chrome),
247            chrome_headless_shell: chrome_binaries.contains(&ChromeBinary::ChromeHeadlessShell),
248        })
249    }
250}
251
252#[derive(Debug)]
253struct DownloadedBrowserArtifacts {
254    chromedriver: PathBuf,
255    chrome: Option<PathBuf>,
256    chrome_headless_shell: Option<PathBuf>,
257}
258
259impl DownloadedBrowserArtifacts {
260    fn package_for(
261        &self,
262        chrome_binary: ChromeBinary,
263        version: Version,
264        platform: Platform,
265    ) -> Result<LoadedBrowserPackage, Report<ChromeForTestingManagerError>> {
266        let browser_executable = self
267            .browser_executable(chrome_binary, version, platform)?
268            .clone();
269
270        Ok(LoadedBrowserPackage::new(
271            chrome_binary,
272            browser_executable,
273            self.chromedriver.clone(),
274        ))
275    }
276
277    fn browser_executable(
278        &self,
279        chrome_binary: ChromeBinary,
280        version: Version,
281        platform: Platform,
282    ) -> Result<&PathBuf, Report<ChromeForTestingManagerError>> {
283        match chrome_binary {
284            ChromeBinary::Chrome => self.chrome.as_ref().ok_or_else(|| {
285                report!(ChromeForTestingManagerError::NoChromeDownload { version, platform })
286            }),
287            ChromeBinary::ChromeHeadlessShell => {
288                self.chrome_headless_shell.as_ref().ok_or_else(|| {
289                    report!(
290                        ChromeForTestingManagerError::NoChromeHeadlessShellDownload {
291                            version,
292                            platform,
293                        }
294                    )
295                })
296            }
297        }
298    }
299}
300
301/// Lower-level orchestrator for chrome-for-testing artifacts.
302///
303/// Most users should use [`crate::Chromedriver`], which wraps this manager with sensible defaults
304/// and handles process lifecycle automatically. Reach for `ChromeForTestingManager` directly when
305/// you need finer control:
306///
307/// - **Pre-warm a cache** without spawning chromedriver: call [`Self::resolve_version`] and
308///   [`Self::download`] with the [`ChromeBinary`] values you need, then drop the result.
309/// - **Run multiple chromedriver instances** off a single resolved version: call
310///   [`Self::launch_chromedriver`] repeatedly with the same [`LoadedBrowserPackage`].
311/// - **Inspect or modify the resolved version** before downloading (channel, available platforms).
312/// - **Pin a custom cache directory** via [`Self::new_with_cache_dir`] (useful in CI).
313/// - **Drive sessions through a non-`thirtyfour`** `WebDriver` client by using the chromedriver
314///   process and port directly.
315#[derive(Debug)]
316pub struct ChromeForTestingManager {
317    client: reqwest::Client,
318    cache_dir: CacheDir,
319    platform: Platform,
320}
321
322impl ChromeForTestingManager {
323    /// Create a manager that uses the platform-default cache directory.
324    ///
325    /// # Errors
326    ///
327    /// Returns an error if the current platform is unsupported or the cache directory
328    /// cannot be determined or created.
329    pub fn new() -> Result<Self, Report<ChromeForTestingManagerError>> {
330        Ok(Self {
331            client: reqwest::Client::new(),
332            cache_dir: CacheDir::get_or_create()?,
333            platform: Platform::detect().map_err(unsupported_platform_error)?,
334        })
335    }
336
337    /// Create a manager that caches downloaded artifacts under `cache_dir`.
338    ///
339    /// The directory is created if it does not exist. Useful in CI to share the cache across
340    /// runs, or to keep artifacts out of the user-default cache location.
341    ///
342    /// # Errors
343    ///
344    /// Returns an error if the current platform is unsupported or the directory cannot be created.
345    pub fn new_with_cache_dir(
346        cache_dir: PathBuf,
347    ) -> Result<Self, Report<ChromeForTestingManagerError>> {
348        Ok(Self {
349            client: reqwest::Client::new(),
350            cache_dir: CacheDir::create_at(cache_dir)?,
351            platform: Platform::detect().map_err(unsupported_platform_error)?,
352        })
353    }
354
355    fn version_dir(&self, version: Version) -> PathBuf {
356        self.cache_dir.path().join(version.to_string())
357    }
358
359    fn platform_dir(&self, version: Version) -> PathBuf {
360        self.version_dir(version).join(self.platform.to_string())
361    }
362
363    async fn ensure_platform_dir(
364        &self,
365        version: Version,
366    ) -> Result<PathBuf, Report<ChromeForTestingManagerError>> {
367        let platform_dir = self.platform_dir(version);
368        fs::create_dir_all(&platform_dir).await.context(
369            ChromeForTestingManagerError::CreatePlatformDir {
370                platform_dir: platform_dir.clone(),
371            },
372        )?;
373        Ok(platform_dir)
374    }
375
376    /// # Errors
377    ///
378    /// Returns an error if the cache directory cannot be deleted or re-created.
379    pub async fn clear_cache(&self) -> Result<(), Report<ChromeForTestingManagerError>> {
380        self.cache_dir.clear().await
381    }
382
383    /// Resolve a [`VersionRequest`] against the chrome-for-testing release index.
384    ///
385    /// Returns a [`SelectedVersion`] suitable for [`Self::download`]. No artifacts are downloaded
386    /// at this point; this only performs the HTTP requests needed to determine which version to
387    /// fetch.
388    ///
389    /// # Errors
390    ///
391    /// Returns an error if the version manifest cannot be fetched or no matching version exists.
392    pub async fn resolve_version(
393        &self,
394        version_selection: VersionRequest,
395    ) -> Result<SelectedVersion, Report<ChromeForTestingManagerError>> {
396        let selected = match &version_selection {
397            VersionRequest::Latest => {
398                let all = KnownGoodVersions::fetch(&self.client)
399                    .await
400                    .map_err(|err| request_versions_error(err, &version_selection))?;
401                all.versions
402                    .iter()
403                    .filter(|v| v.downloads.chromedriver.is_some())
404                    .max_by_key(|v| v.version)
405                    .cloned()
406                    .map(|v| SelectedVersion::from((v, self.platform)))
407            }
408            VersionRequest::LatestIn(channel) => {
409                let all = LastKnownGoodVersions::fetch(&self.client)
410                    .await
411                    .map_err(|err| request_versions_error(err, &version_selection))?;
412                all.channel(channel)
413                    .cloned()
414                    .map(|v| SelectedVersion::from((v, self.platform)))
415            }
416            VersionRequest::Fixed(version) => {
417                let all = KnownGoodVersions::fetch(&self.client)
418                    .await
419                    .map_err(|err| request_versions_error(err, &version_selection))?;
420                all.versions
421                    .into_iter()
422                    .find(|v| v.version == *version)
423                    .map(|v| SelectedVersion::from((v, self.platform)))
424            }
425        };
426
427        let selected = selected.context(ChromeForTestingManagerError::NoMatchingVersion {
428            version_request: version_selection,
429        })?;
430
431        Ok(selected)
432    }
433
434    /// Download the requested browser artifact(s) and matching `ChromeDriver`.
435    ///
436    /// Returns one [`LoadedBrowserPackage`] per requested [`ChromeBinary`], in request order. The
437    /// download work is de-duplicated, so repeated binary values do not download the same artifact
438    /// more than once.
439    ///
440    /// # Errors
441    ///
442    /// Returns an error if `chrome_binaries` is empty, no platform-matching browser or
443    /// `ChromeDriver` download exists, the cache directory cannot be prepared, or the download /
444    /// extraction fails.
445    pub async fn download(
446        &self,
447        selected: &SelectedVersion,
448        chrome_binaries: &[ChromeBinary],
449    ) -> Result<Vec<LoadedBrowserPackage>, Report<ChromeForTestingManagerError>> {
450        let requested = RequestedChromeBinaries::from_slice(chrome_binaries)?;
451        let artifacts = self
452            .download_requested_artifacts(selected, requested)
453            .await?;
454        let mut loaded = Vec::with_capacity(chrome_binaries.len());
455        for chrome_binary in chrome_binaries {
456            loaded.push(artifacts.package_for(*chrome_binary, selected.version, self.platform)?);
457        }
458
459        Ok(loaded)
460    }
461
462    pub(crate) async fn download_one(
463        &self,
464        selected: &SelectedVersion,
465        chrome_binary: ChromeBinary,
466    ) -> Result<LoadedBrowserPackage, Report<ChromeForTestingManagerError>> {
467        let artifacts = self
468            .download_requested_artifacts(selected, RequestedChromeBinaries::single(chrome_binary))
469            .await?;
470        artifacts.package_for(chrome_binary, selected.version, self.platform)
471    }
472
473    async fn download_requested_artifacts(
474        &self,
475        selected: &SelectedVersion,
476        requested: RequestedChromeBinaries,
477    ) -> Result<DownloadedBrowserArtifacts, Report<ChromeForTestingManagerError>> {
478        let platform_dir = self.ensure_platform_dir(selected.version).await?;
479
480        let (chromedriver, chrome, chrome_headless_shell) = tokio::try_join!(
481            self.download_chromedriver(selected, &platform_dir),
482            self.download_requested_browser(
483                selected,
484                &platform_dir,
485                ChromeBinary::Chrome,
486                requested.chrome,
487            ),
488            self.download_requested_browser(
489                selected,
490                &platform_dir,
491                ChromeBinary::ChromeHeadlessShell,
492                requested.chrome_headless_shell,
493            ),
494        )?;
495
496        Ok(DownloadedBrowserArtifacts {
497            chromedriver,
498            chrome,
499            chrome_headless_shell,
500        })
501    }
502
503    async fn download_requested_browser(
504        &self,
505        selected: &SelectedVersion,
506        platform_dir: &Path,
507        chrome_binary: ChromeBinary,
508        is_requested: bool,
509    ) -> Result<Option<PathBuf>, Report<ChromeForTestingManagerError>> {
510        if is_requested {
511            self.download_browser(selected, platform_dir, chrome_binary)
512                .await
513                .map(Some)
514        } else {
515            Ok(None)
516        }
517    }
518
519    async fn download_browser(
520        &self,
521        selected: &SelectedVersion,
522        platform_dir: &Path,
523        chrome_binary: ChromeBinary,
524    ) -> Result<PathBuf, Report<ChromeForTestingManagerError>> {
525        let selected_chrome_download = match chrome_binary {
526            ChromeBinary::Chrome => selected.chrome.clone().ok_or_else(|| {
527                report!(ChromeForTestingManagerError::NoChromeDownload {
528                    version: selected.version,
529                    platform: self.platform,
530                })
531            })?,
532            ChromeBinary::ChromeHeadlessShell => {
533                selected.chrome_headless_shell.clone().ok_or_else(|| {
534                    report!(
535                        ChromeForTestingManagerError::NoChromeHeadlessShellDownload {
536                            version: selected.version,
537                            platform: self.platform,
538                        }
539                    )
540                })?
541            }
542        };
543
544        let chrome_executable = platform_dir.join(chrome_binary.executable_path(self.platform));
545        self.ensure_artifact_downloaded(
546            selected,
547            platform_dir,
548            &chrome_executable,
549            chrome_binary.artifact(),
550            chrome_binary.label(),
551            &selected_chrome_download.url,
552        )
553        .await?;
554
555        Ok(chrome_executable)
556    }
557
558    async fn download_chromedriver(
559        &self,
560        selected: &SelectedVersion,
561        platform_dir: &Path,
562    ) -> Result<PathBuf, Report<ChromeForTestingManagerError>> {
563        let Some(selected_chromedriver_download) = selected.chromedriver.clone() else {
564            bail!(ChromeForTestingManagerError::NoChromedriverDownload {
565                version: selected.version,
566                platform: self.platform,
567            });
568        };
569
570        let chromedriver_executable =
571            platform_dir.join(self.platform.chromedriver_executable_path());
572        self.ensure_artifact_downloaded(
573            selected,
574            platform_dir,
575            &chromedriver_executable,
576            ChromeForTestingArtifact::ChromeDriver,
577            "Chromedriver",
578            &selected_chromedriver_download.url,
579        )
580        .await?;
581
582        Ok(chromedriver_executable)
583    }
584
585    async fn ensure_artifact_downloaded(
586        &self,
587        selected: &SelectedVersion,
588        platform_dir: &Path,
589        executable: &Path,
590        artifact: ChromeForTestingArtifact,
591        label: &str,
592        url: &str,
593    ) -> Result<(), Report<ChromeForTestingManagerError>> {
594        let channel_label = selected
595            .channel
596            .as_ref()
597            .map_or_else(String::new, ToString::to_string);
598
599        if executable.exists() && executable.is_file() {
600            tracing::info!(
601                "{label} {} already installed at {executable:?}...",
602                selected.version
603            );
604        } else {
605            tracing::info!("Installing {channel_label} {label} {}", selected.version);
606            download::download_zip(&self.client, url, platform_dir, platform_dir, artifact).await?;
607        }
608
609        Ok(())
610    }
611
612    /// Launch a chromedriver process from `loaded` on the requested port.
613    ///
614    /// Returns the spawned process handle, the actual bound port (relevant when
615    /// [`PortRequest::Any`] was used), and the long-lived output inspectors that drive the
616    /// optional [`DriverOutputListener`]. Keep the inspectors alive while you want to receive
617    /// output lines.
618    ///
619    /// The returned [`ProcessHandle`] is not auto-terminated. Either wrap it with
620    /// [`ProcessHandle::terminate_on_drop`] or call its `terminate` method explicitly. The
621    /// `shutdown` argument is only used for the internal cleanup path that fires when
622    /// chromedriver fails to report successful startup. Pass the same value you intend to use
623    /// for graceful shutdown so a startup failure honors your tuned budget.
624    ///
625    /// # Errors
626    ///
627    /// Returns an error if the chromedriver binary cannot be spawned or does not report
628    /// successful startup within 10 seconds.
629    ///
630    /// # Panics
631    ///
632    /// Panics if the chromedriver executable path contains non-Unicode bytes.
633    pub async fn launch_chromedriver(
634        &self,
635        loaded: &LoadedBrowserPackage,
636        port: PortRequest,
637        output_listener: Option<DriverOutputListener>,
638        shutdown: GracefulShutdown,
639    ) -> Result<
640        (ManagedProcessHandle, Port, DriverOutputInspectors),
641        Report<ChromeForTestingManagerError>,
642    > {
643        let chromedriver_executable = loaded.chromedriver_executable();
644        let chromedriver_exe_path_str = chromedriver_executable.to_str().expect("valid unicode");
645
646        tracing::info!("Launching chromedriver... {chromedriver_executable:?}");
647        let mut command = Command::new(chromedriver_exe_path_str);
648        match port {
649            PortRequest::Any => {}
650            PortRequest::Specific(port) => {
651                command.arg(format!("--port={}", port.as_u16()));
652            }
653        }
654        let loglevel = chrome_for_testing::chromedriver::LogLevel::Info;
655        command.arg(format!("--log-level={loglevel}"));
656
657        self.apply_chromedriver_creation_flags(&mut command);
658
659        let mut chromedriver_process = Process::new(command)
660            .name("chromedriver")
661            .stdout_and_stderr(|stream| {
662                stream
663                    .broadcast()
664                    .reliable_with_backpressure()
665                    .replay_last_bytes(1.megabytes())
666                    .read_chunk_size(DEFAULT_READ_CHUNK_SIZE)
667                    .max_buffered_chunks(DEFAULT_MAX_BUFFERED_CHUNKS)
668            })
669            .spawn()
670            .context(ChromeForTestingManagerError::SpawnChromedriver {
671                path: chromedriver_executable.to_path_buf(),
672            })?;
673
674        let output_inspectors =
675            DriverOutputInspectors::start(&chromedriver_process, output_listener);
676
677        tracing::info!("Waiting for chromedriver to start...");
678        let started_on_port = Arc::new(AtomicU16::new(0));
679        let started_on_port_clone = started_on_port.clone();
680        let startup_result = chromedriver_process
681            .stdout()
682            .wait_for_line(
683                Duration::from_secs(10),
684                move |line| {
685                    if line.contains("started successfully on port") {
686                        let Some(port) = line
687                            .trim()
688                            .trim_matches('"')
689                            .trim_end_matches('.')
690                            .split(' ')
691                            .next_back()
692                            .and_then(|s| s.parse::<u16>().ok())
693                        else {
694                            tracing::error!(
695                                "Failed to parse port from chromedriver output: {line:?}"
696                            );
697                            return false;
698                        };
699                        started_on_port_clone.store(port, std::sync::atomic::Ordering::Release);
700                        true
701                    } else {
702                        false
703                    }
704                },
705                LineParsingOptions::builder()
706                    .max_line_length(DEFAULT_MAX_LINE_LENGTH)
707                    .overflow_behavior(LineOverflowBehavior::DropAdditionalData)
708                    .buffer_compaction_threshold(None)
709                    .build(),
710            )
711            .await
712            .context(ChromeForTestingManagerError::WaitForChromedriverStartup {
713                path: chromedriver_executable.to_path_buf(),
714            })?;
715        match startup_result {
716            WaitForLineResult::Matched => {}
717            WaitForLineResult::StreamClosed | WaitForLineResult::Timeout => {
718                if let Err(err) = chromedriver_process.terminate(shutdown).await {
719                    tracing::warn!(
720                        error = %err,
721                        "failed to terminate chromedriver after startup failure"
722                    );
723                }
724
725                return Err(report!(
726                    ChromeForTestingManagerError::WaitForChromedriverStartup {
727                        path: chromedriver_executable.to_path_buf(),
728                    }
729                ));
730            }
731        }
732
733        // It SHOULD definitely be terminated.
734        // But the default implementation when "must_be_terminated" raises a panic if not terminated.
735        // Our custom `Drop` impl on `Chromedriver` relaxes this and only logs an ERROR instead.
736        chromedriver_process.must_not_be_terminated();
737
738        Ok((
739            chromedriver_process,
740            Port::new(Arc::into_inner(started_on_port).unwrap().into_inner()),
741            output_inspectors,
742        ))
743    }
744
745    /// Launch Chrome Headless Shell for a single attached `WebDriver` session.
746    ///
747    /// Headless Shell starts with no pages when `ChromeDriver` launches it directly. For that
748    /// binary, start the browser first, create an initial blank page through `DevTools`, and let
749    /// `ChromeDriver` attach via `debuggerAddress`.
750    #[cfg(feature = "thirtyfour")]
751    pub(crate) async fn launch_headless_shell_session(
752        &self,
753        loaded: &LoadedChromeHeadlessShellPackage,
754        caps: &thirtyfour::ChromeCapabilities,
755        shutdown: GracefulShutdown,
756    ) -> Result<HeadlessShellSession, Report<ChromeForTestingManagerError>> {
757        let chrome_headless_shell_executable =
758            loaded.chrome_headless_shell_executable().to_path_buf();
759        let chrome_headless_shell_executable_str = chrome_headless_shell_executable
760            .to_str()
761            .expect("valid unicode");
762        tracing::info!("Launching Chrome Headless Shell... {chrome_headless_shell_executable:?}");
763
764        let mut command = Command::new(chrome_headless_shell_executable_str);
765        command.args(headless_shell_launch_args(caps)?);
766
767        let mut browser_process = Process::new(command)
768            .name("chrome-headless-shell")
769            .stdout_and_stderr(|stream| {
770                stream
771                    .broadcast()
772                    .reliable_with_backpressure()
773                    .replay_last_bytes(1.megabytes())
774                    .read_chunk_size(DEFAULT_READ_CHUNK_SIZE)
775                    .max_buffered_chunks(DEFAULT_MAX_BUFFERED_CHUNKS)
776            })
777            .spawn()
778            .context(ChromeForTestingManagerError::SpawnBrowser {
779                path: chrome_headless_shell_executable.clone(),
780            })?;
781
782        let debugger_address = match wait_for_devtools_address(
783            &mut browser_process,
784            &chrome_headless_shell_executable,
785        )
786        .await
787        {
788            Ok(debugger_address) => debugger_address,
789            Err(err) => {
790                terminate_browser_after_startup_failure(&mut browser_process, shutdown).await;
791                return Err(err);
792            }
793        };
794
795        if let Err(err) = self.create_initial_browser_page(&debugger_address).await {
796            terminate_browser_after_startup_failure(&mut browser_process, shutdown).await;
797            return Err(err);
798        }
799
800        Ok(HeadlessShellSession {
801            process: browser_process.terminate_on_drop(shutdown.clone()),
802            debugger_address,
803            shutdown,
804        })
805    }
806
807    #[cfg(target_os = "windows")]
808    #[expect(clippy::unused_self)]
809    fn apply_chromedriver_creation_flags<'a>(&self, command: &'a mut Command) -> &'a mut Command {
810        // CREATE_NO_WINDOW (0x08000000) is a Windows-specific process creation flag that prevents
811        // a process from creating a new window. This is relevant for ChromeDriver because:
812        //   - ChromeDriver is typically a console application on Windows.
813        //   - Without this flag, launching ChromeDriver would create a visible console window.
814        //   - In our automation scenario, we don't want users to see this console window popping up.
815        //   - The window isn't necessary since we're already capturing the stdout/stderr streams programmatically.
816        const CREATE_NO_WINDOW: u32 = 0x0800_0000;
817
818        command.creation_flags(CREATE_NO_WINDOW)
819    }
820
821    #[cfg(not(target_os = "windows"))]
822    #[expect(clippy::unused_self)]
823    fn apply_chromedriver_creation_flags<'a>(&self, command: &'a mut Command) -> &'a mut Command {
824        command
825    }
826
827    /// Prepare a [`thirtyfour::ChromeCapabilities`] pre-wired with the loaded Chrome binary path
828    /// and the headless flag set.
829    ///
830    /// Useful when constructing a [`thirtyfour::WebDriver`] manually instead of using
831    /// [`crate::Chromedriver::session`].
832    ///
833    /// # Errors
834    ///
835    /// Returns an error if the underlying capability setup fails.
836    ///
837    /// # Panics
838    ///
839    /// Panics if the cached Chrome executable path contains non-Unicode bytes.
840    #[cfg(feature = "thirtyfour")]
841    #[allow(clippy::unused_self)] // Takes &self for API consistency with other methods.
842    pub fn prepare_caps(
843        &self,
844        loaded: &LoadedBrowserPackage,
845    ) -> Result<thirtyfour::ChromeCapabilities, Report<ChromeForTestingManagerError>> {
846        use thirtyfour::ChromiumLikeCapabilities;
847
848        let browser_executable = loaded.browser_executable();
849        tracing::debug!("Registering {browser_executable:?} in capabilities.");
850        let mut caps = thirtyfour::ChromeCapabilities::new();
851        caps.set_headless()
852            .context(ChromeForTestingManagerError::PrepareChromeCapabilities {
853                browser_executable: browser_executable.to_path_buf(),
854            })?;
855        caps.set_binary(browser_executable.to_str().expect("valid unicode"))
856            .context(ChromeForTestingManagerError::PrepareChromeCapabilities {
857                browser_executable: browser_executable.to_path_buf(),
858            })?;
859        Ok(caps)
860    }
861}
862
863#[cfg(feature = "thirtyfour")]
864fn headless_shell_launch_args(
865    caps: &thirtyfour::ChromeCapabilities,
866) -> Result<Vec<String>, Report<ChromeForTestingManagerError>> {
867    use thirtyfour::BrowserCapabilitiesHelper;
868
869    let mut launch_args = Vec::new();
870    let mut remote_debugging_port_arg = None::<String>;
871
872    for arg in caps.args() {
873        match classify_remote_debugging_arg(&arg) {
874            Some(RemoteDebuggingArg::Pipe) => {
875                return Err(report!(
876                    ChromeForTestingManagerError::UnsupportedHeadlessShellRemoteDebuggingArg {
877                        arg,
878                    }
879                ));
880            }
881            Some(RemoteDebuggingArg::InvalidPort) => {
882                return Err(report!(
883                    ChromeForTestingManagerError::InvalidHeadlessShellRemoteDebuggingPortArg {
884                        arg,
885                    }
886                ));
887            }
888            Some(RemoteDebuggingArg::Port) => {
889                if let Some(first_arg) = &remote_debugging_port_arg {
890                    return Err(report!(
891                        ChromeForTestingManagerError::ConflictingHeadlessShellRemoteDebuggingArgs {
892                            first_arg: first_arg.clone(),
893                            second_arg: arg,
894                        }
895                    ));
896                }
897                remote_debugging_port_arg = Some(arg);
898            }
899            None => launch_args.push(arg),
900        }
901    }
902
903    launch_args.push(
904        remote_debugging_port_arg
905            .unwrap_or_else(|| DEFAULT_HEADLESS_SHELL_REMOTE_DEBUGGING_ARG.to_owned()),
906    );
907    Ok(launch_args)
908}
909
910#[cfg(feature = "thirtyfour")]
911#[derive(Debug, Clone, Copy, PartialEq, Eq)]
912enum RemoteDebuggingArg {
913    Pipe,
914    Port,
915    InvalidPort,
916}
917
918#[cfg(feature = "thirtyfour")]
919fn classify_remote_debugging_arg(arg: &str) -> Option<RemoteDebuggingArg> {
920    if arg == "--remote-debugging-pipe" || arg.starts_with("--remote-debugging-pipe=") {
921        return Some(RemoteDebuggingArg::Pipe);
922    }
923
924    if arg == "--remote-debugging-port" {
925        return Some(RemoteDebuggingArg::InvalidPort);
926    }
927
928    let port = arg.strip_prefix("--remote-debugging-port=")?;
929
930    if port.parse::<u16>().is_ok() {
931        Some(RemoteDebuggingArg::Port)
932    } else {
933        Some(RemoteDebuggingArg::InvalidPort)
934    }
935}
936
937#[cfg(feature = "thirtyfour")]
938async fn wait_for_devtools_address(
939    browser_process: &mut ManagedProcessHandle,
940    chrome_executable: &Path,
941) -> Result<String, Report<ChromeForTestingManagerError>> {
942    let debugger_address = Arc::new(Mutex::new(None::<String>));
943    let debugger_address_for_wait = debugger_address.clone();
944    let recent_output = Arc::new(Mutex::new(VecDeque::new()));
945    let recent_output_for_wait = recent_output.clone();
946    let startup_result = match browser_process
947        .stderr()
948        .wait_for_line(
949            Duration::from_secs(10),
950            move |line| {
951                push_recent_browser_output(&recent_output_for_wait, line.as_ref());
952                let Some(address) = parse_devtools_address(&line) else {
953                    return false;
954                };
955                let mut stored = debugger_address_for_wait.lock().expect("not poisoned");
956                *stored = Some(address);
957                true
958            },
959            LineParsingOptions::builder()
960                .max_line_length(DEFAULT_MAX_LINE_LENGTH)
961                .overflow_behavior(LineOverflowBehavior::DropAdditionalData)
962                .buffer_compaction_threshold(None)
963                .build(),
964        )
965        .await
966    {
967        Ok(result) => result,
968        Err(err) => {
969            log_recent_browser_startup_output(chrome_executable, &recent_output);
970            return Err(Report::new_sendsync(err).context(
971                ChromeForTestingManagerError::WaitForBrowserStartup {
972                    path: chrome_executable.to_path_buf(),
973                },
974            ));
975        }
976    };
977
978    match startup_result {
979        WaitForLineResult::Matched => debugger_address
980            .lock()
981            .expect("not poisoned")
982            .clone()
983            .context(ChromeForTestingManagerError::WaitForBrowserStartup {
984                path: chrome_executable.to_path_buf(),
985            }),
986        WaitForLineResult::StreamClosed | WaitForLineResult::Timeout => {
987            log_recent_browser_startup_output(chrome_executable, &recent_output);
988            Err(report!(
989                ChromeForTestingManagerError::WaitForBrowserStartup {
990                    path: chrome_executable.to_path_buf(),
991                }
992            ))
993        }
994    }
995}
996
997#[cfg(feature = "thirtyfour")]
998fn push_recent_browser_output(recent_output: &RecentBrowserOutput, line: &str) {
999    let mut recent_output = recent_output.lock().expect("not poisoned");
1000    if recent_output.len() == BROWSER_STARTUP_OUTPUT_LINES {
1001        recent_output.pop_front();
1002    }
1003    recent_output.push_back(line.to_owned());
1004}
1005
1006#[cfg(feature = "thirtyfour")]
1007fn log_recent_browser_startup_output(
1008    chrome_executable: &Path,
1009    recent_output: &RecentBrowserOutput,
1010) {
1011    let recent_output = recent_output.lock().expect("not poisoned");
1012    if recent_output.is_empty() {
1013        tracing::error!(
1014            path = %chrome_executable.display(),
1015            "Chrome Headless Shell exited before DevTools startup and produced no captured stderr"
1016        );
1017        return;
1018    }
1019
1020    tracing::error!(
1021        path = %chrome_executable.display(),
1022        "Chrome Headless Shell startup output before DevTools startup failed:\n{}",
1023        recent_output
1024            .iter()
1025            .map(String::as_str)
1026            .collect::<Vec<_>>()
1027            .join("\n")
1028    );
1029}
1030
1031#[cfg(feature = "thirtyfour")]
1032fn parse_devtools_address(line: &str) -> Option<String> {
1033    let (_, after_prefix) = line.split_once("DevTools listening on ws://")?;
1034    let (address, _) = after_prefix.split_once('/')?;
1035    if address.is_empty() {
1036        None
1037    } else {
1038        Some(address.to_owned())
1039    }
1040}
1041
1042#[cfg(feature = "thirtyfour")]
1043async fn terminate_browser_after_startup_failure(
1044    browser_process: &mut ManagedProcessHandle,
1045    shutdown: GracefulShutdown,
1046) {
1047    if let Err(err) = browser_process.terminate(shutdown).await {
1048        tracing::warn!(
1049            error = %err,
1050            "failed to terminate browser after startup failure"
1051        );
1052    }
1053}
1054
1055#[cfg(feature = "thirtyfour")]
1056impl ChromeForTestingManager {
1057    async fn create_initial_browser_page(
1058        &self,
1059        debugger_address: &str,
1060    ) -> Result<(), Report<ChromeForTestingManagerError>> {
1061        self.client
1062            .put(format!("http://{debugger_address}/json/new?about:blank"))
1063            .send()
1064            .await
1065            .context(ChromeForTestingManagerError::CreateInitialBrowserPage {
1066                debugger_address: debugger_address.to_owned(),
1067            })?
1068            .error_for_status()
1069            .context(ChromeForTestingManagerError::CreateInitialBrowserPage {
1070                debugger_address: debugger_address.to_owned(),
1071            })?;
1072        Ok(())
1073    }
1074}
1075
1076fn unsupported_platform_error(err: impl std::fmt::Display) -> Report<ChromeForTestingManagerError> {
1077    report!(ChromeForTestingManagerError::UnsupportedPlatform)
1078        .attach(format!("chrome-for-testing error:\n{err}"))
1079}
1080
1081fn request_versions_error(
1082    err: impl std::fmt::Display,
1083    version_request: &VersionRequest,
1084) -> Report<ChromeForTestingManagerError> {
1085    report!(ChromeForTestingManagerError::RequestVersions {
1086        version_request: version_request.clone(),
1087    })
1088    .attach(format!("chrome-for-testing error:\n{err}"))
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093    use crate::chromedriver::default_graceful_shutdown;
1094    use crate::mgr::{ChromeBinary, ChromeForTestingManager, LoadedBrowserPackage};
1095    use crate::mgr::{
1096        DEFAULT_HEADLESS_SHELL_REMOTE_DEBUGGING_ARG, RemoteDebuggingArg,
1097        classify_remote_debugging_arg, headless_shell_launch_args, parse_devtools_address,
1098    };
1099    use crate::port::Port;
1100    use crate::port::PortRequest;
1101    use crate::version::SelectedVersion;
1102    use crate::{Channel, Version, VersionRequest};
1103    use assertr::prelude::*;
1104    use rootcause::Report;
1105    use serial_test::serial;
1106    use std::path::{Path, PathBuf};
1107    use thirtyfour::ChromiumLikeCapabilities;
1108
1109    #[ctor::ctor(unsafe)]
1110    fn init_test_tracing() {
1111        tracing_subscriber::fmt().with_test_writer().try_init().ok();
1112    }
1113
1114    #[tokio::test(flavor = "multi_thread")]
1115    #[serial]
1116    async fn clear_cache_and_download_new() -> Result<(), Report> {
1117        let mgr = ChromeForTestingManager::new()?;
1118        mgr.clear_cache().await?;
1119        let selected = mgr
1120            .resolve_version(VersionRequest::LatestIn(Channel::Stable))
1121            .await?;
1122        let loaded = download_regular_chrome(&mgr, selected).await?;
1123
1124        assert_that!(loaded.browser_executable())
1125            .exists()
1126            .is_a_file();
1127        assert_that!(loaded.chromedriver_executable())
1128            .exists()
1129            .is_a_file();
1130        assert_that!(loaded.chrome_binary()).is_equal_to(ChromeBinary::Chrome);
1131        Ok(())
1132    }
1133
1134    #[tokio::test(flavor = "multi_thread")]
1135    #[serial]
1136    async fn resolve_and_download_latest() -> Result<(), Report> {
1137        let mgr = ChromeForTestingManager::new()?;
1138        let selected = mgr.resolve_version(VersionRequest::Latest).await?;
1139        let loaded = download_regular_chrome(&mgr, selected).await?;
1140
1141        assert_that!(loaded.browser_executable())
1142            .exists()
1143            .is_a_file();
1144        assert_that!(loaded.chromedriver_executable())
1145            .exists()
1146            .is_a_file();
1147        assert_that!(loaded.chrome_binary()).is_equal_to(ChromeBinary::Chrome);
1148        Ok(())
1149    }
1150
1151    #[tokio::test(flavor = "multi_thread")]
1152    #[serial]
1153    async fn resolve_and_download_latest_in_stable_channel() -> Result<(), Report> {
1154        let mgr = ChromeForTestingManager::new()?;
1155        let selected = mgr
1156            .resolve_version(VersionRequest::LatestIn(Channel::Stable))
1157            .await?;
1158        let loaded = download_regular_chrome(&mgr, selected).await?;
1159
1160        assert_that!(loaded.browser_executable())
1161            .exists()
1162            .is_a_file();
1163        assert_that!(loaded.chromedriver_executable())
1164            .exists()
1165            .is_a_file();
1166        assert_that!(loaded.chrome_binary()).is_equal_to(ChromeBinary::Chrome);
1167        Ok(())
1168    }
1169
1170    #[tokio::test(flavor = "multi_thread")]
1171    #[serial]
1172    async fn resolve_and_download_specific() -> Result<(), Report> {
1173        let mgr = ChromeForTestingManager::new()?;
1174        let selected = mgr
1175            .resolve_version(VersionRequest::Fixed(Version {
1176                major: 135,
1177                minor: 0,
1178                patch: 7019,
1179                build: 0,
1180            }))
1181            .await?;
1182        let loaded = download_regular_chrome(&mgr, selected).await?;
1183
1184        assert_that!(loaded.browser_executable())
1185            .exists()
1186            .is_a_file();
1187        assert_that!(loaded.chromedriver_executable())
1188            .exists()
1189            .is_a_file();
1190        assert_that!(loaded.chrome_binary()).is_equal_to(ChromeBinary::Chrome);
1191        Ok(())
1192    }
1193
1194    #[test]
1195    fn loaded_browser_package_preserves_browser_type_and_paths() {
1196        let chrome_package = LoadedBrowserPackage::new(
1197            ChromeBinary::Chrome,
1198            PathBuf::from("/cache/chrome"),
1199            PathBuf::from("/cache/chromedriver"),
1200        );
1201        assert_that!(chrome_package.chrome_binary()).is_equal_to(ChromeBinary::Chrome);
1202        assert_that!(chrome_package.browser_executable()).is_equal_to(Path::new("/cache/chrome"));
1203        assert_that!(chrome_package.chromedriver_executable())
1204            .is_equal_to(Path::new("/cache/chromedriver"));
1205        assert_that!(matches!(chrome_package, LoadedBrowserPackage::Chrome(_))).is_true();
1206
1207        let headless_shell_package = LoadedBrowserPackage::new(
1208            ChromeBinary::ChromeHeadlessShell,
1209            PathBuf::from("/cache/chrome-headless-shell"),
1210            PathBuf::from("/cache/chromedriver"),
1211        );
1212        assert_that!(headless_shell_package.chrome_binary())
1213            .is_equal_to(ChromeBinary::ChromeHeadlessShell);
1214        assert_that!(headless_shell_package.browser_executable())
1215            .is_equal_to(Path::new("/cache/chrome-headless-shell"));
1216        assert_that!(headless_shell_package.chromedriver_executable())
1217            .is_equal_to(Path::new("/cache/chromedriver"));
1218        assert_that!(matches!(
1219            headless_shell_package,
1220            LoadedBrowserPackage::ChromeHeadlessShell(_)
1221        ))
1222        .is_true();
1223    }
1224
1225    #[tokio::test(flavor = "multi_thread")]
1226    async fn download_reports_missing_chrome_binary() -> Result<(), Report> {
1227        let mgr = ChromeForTestingManager::new()?;
1228
1229        assert_that!(
1230            mgr.download(&selected_without_downloads(), &[ChromeBinary::Chrome])
1231                .await
1232        )
1233        .is_err();
1234        Ok(())
1235    }
1236
1237    #[tokio::test(flavor = "multi_thread")]
1238    async fn download_reports_missing_chrome_headless_shell_binary() -> Result<(), Report> {
1239        let mgr = ChromeForTestingManager::new()?;
1240
1241        assert_that!(
1242            mgr.download(
1243                &selected_without_downloads(),
1244                &[ChromeBinary::ChromeHeadlessShell]
1245            )
1246            .await
1247        )
1248        .is_err();
1249        Ok(())
1250    }
1251
1252    #[tokio::test(flavor = "multi_thread")]
1253    async fn download_reports_missing_binary_for_combined_request() -> Result<(), Report> {
1254        let mgr = ChromeForTestingManager::new()?;
1255
1256        assert_that!(
1257            mgr.download(
1258                &selected_without_downloads(),
1259                &[ChromeBinary::Chrome, ChromeBinary::ChromeHeadlessShell]
1260            )
1261            .await
1262        )
1263        .is_err();
1264        Ok(())
1265    }
1266
1267    #[tokio::test(flavor = "multi_thread")]
1268    async fn download_reports_empty_browser_request_before_downloading() -> Result<(), Report> {
1269        let mgr = ChromeForTestingManager::new()?;
1270        let result = mgr.download(&selected_without_downloads(), &[]).await;
1271
1272        assert_that!(result)
1273            .is_err()
1274            .derive(ToString::to_string)
1275            .contains("at least one Chrome binary must be requested");
1276        Ok(())
1277    }
1278
1279    #[tokio::test(flavor = "multi_thread")]
1280    #[serial]
1281    async fn launch_chromedriver_on_specific_port() -> Result<(), Report> {
1282        let mgr = ChromeForTestingManager::new()?;
1283        let selected = mgr.resolve_version(VersionRequest::Latest).await?;
1284        let loaded = download_regular_chrome(&mgr, selected).await?;
1285        let (chromedriver, port, _output_inspectors) = mgr
1286            .launch_chromedriver(
1287                &loaded,
1288                PortRequest::Specific(Port::new(3333)),
1289                None,
1290                default_graceful_shutdown(),
1291            )
1292            .await?;
1293        let _chromedriver = chromedriver.terminate_on_drop(default_graceful_shutdown());
1294        assert_that!(port).is_equal_to(Port::new(3333));
1295        Ok(())
1296    }
1297
1298    #[tokio::test(flavor = "multi_thread")]
1299    #[serial]
1300    async fn download_and_launch_chromedriver_on_random_port_and_prepare_thirtyfour_webdriver()
1301    -> Result<(), Report> {
1302        let mgr = ChromeForTestingManager::new()?;
1303        let selected = mgr.resolve_version(VersionRequest::Latest).await?;
1304        let loaded = download_regular_chrome(&mgr, selected).await?;
1305        let (chromedriver, port, _output_inspectors) = mgr
1306            .launch_chromedriver(&loaded, PortRequest::Any, None, default_graceful_shutdown())
1307            .await?;
1308        let _chromedriver = chromedriver.terminate_on_drop(default_graceful_shutdown());
1309
1310        let caps = mgr.prepare_caps(&loaded)?;
1311        let driver = thirtyfour::WebDriver::new(format!("http://localhost:{port}"), caps).await?;
1312        driver.goto("https://www.google.com").await?;
1313
1314        let url = driver.current_url().await?;
1315        assert_that!(url).has_display_value("https://www.google.com/");
1316
1317        driver.quit().await?;
1318
1319        Ok(())
1320    }
1321
1322    #[test]
1323    fn parse_devtools_address_extracts_http_debugger_address() {
1324        assert_that!(parse_devtools_address(
1325            "DevTools listening on ws://127.0.0.1:9222/devtools/browser/abc"
1326        ))
1327        .is_equal_to(Some(String::from("127.0.0.1:9222")));
1328    }
1329
1330    #[test]
1331    fn remote_debugging_args_are_classified_for_headless_shell_sessions() {
1332        assert_that!(classify_remote_debugging_arg("--remote-debugging-pipe"))
1333            .is_equal_to(Some(RemoteDebuggingArg::Pipe));
1334        assert_that!(classify_remote_debugging_arg(
1335            "--remote-debugging-pipe=true"
1336        ))
1337        .is_equal_to(Some(RemoteDebuggingArg::Pipe));
1338        assert_that!(classify_remote_debugging_arg("--remote-debugging-port"))
1339            .is_equal_to(Some(RemoteDebuggingArg::InvalidPort));
1340        assert_that!(classify_remote_debugging_arg("--remote-debugging-port=0"))
1341            .is_equal_to(Some(RemoteDebuggingArg::Port));
1342        assert_that!(classify_remote_debugging_arg(
1343            "--remote-debugging-port=9222"
1344        ))
1345        .is_equal_to(Some(RemoteDebuggingArg::Port));
1346        assert_that!(classify_remote_debugging_arg("--remote-debugging-port="))
1347            .is_equal_to(Some(RemoteDebuggingArg::InvalidPort));
1348        assert_that!(classify_remote_debugging_arg(
1349            "--remote-debugging-port=localhost:9222"
1350        ))
1351        .is_equal_to(Some(RemoteDebuggingArg::InvalidPort));
1352        assert_that!(classify_remote_debugging_arg("--remote-debugging-portable"))
1353            .is_equal_to(None);
1354        assert_that!(classify_remote_debugging_arg("--headless")).is_equal_to(None);
1355    }
1356
1357    #[test]
1358    fn headless_shell_launch_args_add_default_remote_debugging_port() -> Result<(), Report> {
1359        let mut caps = thirtyfour::ChromeCapabilities::new();
1360        caps.add_arg("--headless=new")?;
1361        caps.add_arg("--disable-gpu")?;
1362
1363        assert_that!(headless_shell_launch_args(&caps)?.as_slice()).contains_exactly([
1364            "--headless=new",
1365            "--disable-gpu",
1366            DEFAULT_HEADLESS_SHELL_REMOTE_DEBUGGING_ARG,
1367        ]);
1368        Ok(())
1369    }
1370
1371    #[test]
1372    fn headless_shell_launch_args_use_configured_remote_debugging_port() -> Result<(), Report> {
1373        let mut caps = thirtyfour::ChromeCapabilities::new();
1374        caps.add_arg("--headless=new")?;
1375        caps.add_arg("--remote-debugging-port=9222")?;
1376
1377        assert_that!(headless_shell_launch_args(&caps)?.as_slice())
1378            .contains_exactly(["--headless=new", "--remote-debugging-port=9222"]);
1379        Ok(())
1380    }
1381
1382    #[test]
1383    fn headless_shell_launch_args_reject_remote_debugging_pipe() -> Result<(), Report> {
1384        let mut caps = thirtyfour::ChromeCapabilities::new();
1385        caps.add_arg("--remote-debugging-pipe")?;
1386
1387        assert_that!(headless_shell_launch_args(&caps))
1388            .is_err()
1389            .derive(ToString::to_string)
1390            .contains("unsupported argument \"--remote-debugging-pipe\"");
1391        Ok(())
1392    }
1393
1394    #[test]
1395    fn headless_shell_launch_args_reject_invalid_remote_debugging_port() -> Result<(), Report> {
1396        let mut caps = thirtyfour::ChromeCapabilities::new();
1397        caps.add_arg("--remote-debugging-port")?;
1398
1399        assert_that!(headless_shell_launch_args(&caps))
1400            .is_err()
1401            .derive(ToString::to_string)
1402            .contains("invalid argument \"--remote-debugging-port\"");
1403        Ok(())
1404    }
1405
1406    #[test]
1407    fn headless_shell_launch_args_reject_conflicting_remote_debugging_ports() -> Result<(), Report>
1408    {
1409        let mut caps = thirtyfour::ChromeCapabilities::new();
1410        caps.add_arg("--remote-debugging-port=9222")?;
1411        caps.add_arg("--remote-debugging-port=9223")?;
1412
1413        assert_that!(headless_shell_launch_args(&caps))
1414            .is_err()
1415            .derive(ToString::to_string)
1416            .contains(
1417                "conflicting arguments \"--remote-debugging-port=9222\" and \"--remote-debugging-port=9223\"",
1418            );
1419        Ok(())
1420    }
1421
1422    fn selected_without_downloads() -> SelectedVersion {
1423        SelectedVersion {
1424            channel: None,
1425            version: Version {
1426                major: 135,
1427                minor: 0,
1428                patch: 7019,
1429                build: 0,
1430            },
1431            chrome: None,
1432            chrome_headless_shell: None,
1433            chromedriver: None,
1434        }
1435    }
1436
1437    async fn download_regular_chrome(
1438        mgr: &ChromeForTestingManager,
1439        selected: SelectedVersion,
1440    ) -> Result<LoadedBrowserPackage, Report> {
1441        let loaded = mgr.download(&selected, &[ChromeBinary::Chrome]).await?;
1442        Ok(loaded
1443            .into_iter()
1444            .next()
1445            .expect("one requested binary returns one package"))
1446    }
1447}