Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Failure Artifacts And Logs

Diagnostics are most useful when their collection does not prevent browser cleanup or replace the original test error. These recipes keep those outcomes separate.

Capture A Bounded Failure Report

Create FailureArtifactCollector before the test body so it can record live managed-driver-process output. Its defaults capture URL, title, PNG bytes, a page-source excerpt capped at 64 KiB, and the newest 200 browser and driver process log entries. Driver-provided log text also has per-entry and total UTF-8 byte limits.

#![allow(unused)]
fn main() {
use thirtyfour::{
    prelude::*,
    testing::{BrowserTestError, FailureArtifactCollector, run_browser_test},
};

#[tokio::test]
async fn diagnoses_checkout_failure() -> Result<(), BrowserTestError> {
    run_browser_test(
        WebDriver::managed(DesiredCapabilities::chrome()),
        |driver| async move {
            let collector = FailureArtifactCollector::new(&driver);
            let result: WebDriverResult<()> = async {
                driver.goto("https://example.test/checkout").await?;
                driver
                    .query(By::Testid("order-confirmation"))
                    .with_text("Order received")
                    .single()
                    .await?;
                Ok(())
            }
            .await;

            if result.is_err() {
                // Capture while the session is alive, before the runner quits it.
                eprintln!("{}", collector.capture().await);
            }
            result
        },
    )
    .await
}
}

Capture is best-effort and never fail-fast. Every field is independently Captured, Unavailable, or Disabled, so an unsupported browser-log endpoint does not hide a screenshot or source excerpt. The Display report is intended for CI and AI debugging: it prints only the screenshot byte count, never PNG bytes or base64. The screenshot bytes remain available on FailureArtifacts for writing to a CI artifact file.

This example captures returned Err values only. A panicking assertion unwinds past the capture() call, although run_browser_test still catches the unwind to quit the session before resuming it. Prefer fallible query checks when you need this report, or add a panic-aware wrapper or test-harness hook that calls capture() before cleanup.

Browser logs require logging capabilities set before session creation and are not supported by every driver. Managed-driver-process logs are available only with the manager feature, for managed sessions, and only after collector attachment. The manager may reuse one driver process across sessions, so this process-scoped stream can include lines from another concurrent session. Avoid sharing a managed driver process when per-session log isolation is required. CDP and BiDi diagnostics are intentionally outside this portable baseline. Customize or disable capture through FailureArtifactConfig; configured byte and entry limits remain hard bounds and UTF-8 truncation never splits a code point.

URLs, titles, source excerpts, screenshots, and logs can still contain secrets or personal data. Disable fields that are unsafe for your application, apply application-specific redaction before uploading artifacts, and do not treat the default size bounds as a privacy filter.

Take A Screenshot On Failure

#![allow(unused)]
fn main() {
use std::path::Path;
use thirtyfour::prelude::*;

#[tokio::test]
async fn captures_a_failed_checkout() -> WebDriverResult<()> {
    let driver = WebDriver::managed(DesiredCapabilities::chrome()).await?;

    let test_result: WebDriverResult<()> = async {
        driver.goto("https://example.test/checkout").await?;
        driver
            .query(By::Testid("order-confirmation"))
            .with_text("Order received")
            .and_displayed()
            .desc("completed order confirmation")
            .single()
            .await?;
        Ok(())
    }
    .await;

    let screenshot_result = if test_result.is_err() {
        driver.screenshot(Path::new("checkout-failure.png")).await
    } else {
        Ok(())
    };
    let quit_result = driver.quit().await;

    test_result?;
    screenshot_result?;
    quit_result
}
}

Create the artifact directory before the test if the path includes one. The ordering above always attempts quit(), then gives the original test failure priority over a screenshot or cleanup failure. For a larger suite, move this shape into the test fixture rather than duplicating it in every test.

This result-capture shape handles returned Err values, not Rust panics from assert!. Prefer query-based assertions like the one above, or use a panic-aware test harness when screenshotting panics is also required.

Read Browser And Driver Logs

The legacy browser-log endpoint is implemented by Chromium drivers, not geckodriver. Enable goog:loggingPrefs before creating the session. Driver process logs are separate and are available for locally managed sessions.

#![allow(unused)]
fn main() {
use std::time::Duration;
use tokio::time::{Instant, sleep};
use thirtyfour::{LoggingPrefsLogLevel, prelude::*};

#[tokio::test]
async fn records_chromium_logs() -> WebDriverResult<()> {
    let mut caps = DesiredCapabilities::chrome();
    caps.set_browser_log_level(LoggingPrefsLogLevel::All)?;
    let driver = WebDriver::managed(caps)
        .on_driver_log(|entry| eprintln!("chromedriver: {}", entry.line))
        .await?;

    let test_result: WebDriverResult<()> = async {
        driver
            .goto("data:text/html,<script>console.error('checkout failed')</script>")
            .await?;

        // Log delivery is asynchronous and each call drains the buffer, so
        // accumulate entries until the expected marker arrives or times out.
        let deadline = Instant::now() + Duration::from_secs(5);
        let mut entries = Vec::new();
        loop {
            entries.extend(driver.browser_log().await?);
            if entries.iter().any(|entry| entry.message.contains("checkout failed")) {
                break;
            }
            if Instant::now() >= deadline {
                return Err(WebDriverError::Timeout("browser log marker did not arrive".into()));
            }
            sleep(Duration::from_millis(100)).await;
        }

        for entry in entries {
            println!("[{}] {}", entry.level, entry.message);
        }
        Ok(())
    }
    .await;

    let quit_result = driver.quit().await;
    test_result?;
    quit_result
}
}

For cross-browser console events, prefer the BiDi log subscription described in BiDi Events. For manager status events, log buffering, and per-session driver subscriptions, see WebDriver Manager. Driver-process subscriptions require the default-on manager feature and the manager’s default StdioMode::Tracing; Inherit and Null do not create the stdout/stderr pumps that feed callbacks. The short polling interval above is intentional timing behavior for asynchronous log delivery, not a page-readiness sleep.