thirtyfour 0.37.0

Thirtyfour is a Selenium / WebDriver library for Rust, for automated website UI testing. Tested on Chrome and Firefox, but any webdriver-capable browser should work.
Documentation
//! `Page` domain — navigation, screenshots, lifecycle events.

use serde::{Deserialize, Serialize};

use crate::cdp::Cdp;
use crate::cdp::command::{CdpCommand, CdpEvent, Empty};
use crate::cdp::ids::{FrameId, LoaderId, ScriptId};
use crate::common::protocol::string_enum;
use crate::error::WebDriverResult;

string_enum! {
    /// Image format for [`CaptureScreenshot`].
    pub enum ImageFormat {
        /// PNG (default).
        Png = "png",
        /// JPEG; honours [`CaptureScreenshot::quality`].
        Jpeg = "jpeg",
        /// WebP; honours [`CaptureScreenshot::quality`].
        Webp = "webp",
    }
}

string_enum! {
    /// Hint about how a navigation was initiated. Mirrors CDP's
    /// `Page.TransitionType`.
    pub enum TransitionType {
        /// Navigated by clicking a link.
        Link = "link",
        /// User typed the URL into the address bar.
        Typed = "typed",
        /// Address bar suggestion.
        AddressBar = "address_bar",
        /// Auto-bookmarked page.
        AutoBookmark = "auto_bookmark",
        /// Subframe navigation (automatic).
        AutoSubframe = "auto_subframe",
        /// Subframe navigation (manual).
        ManualSubframe = "manual_subframe",
        /// Navigation generated by the browser.
        Generated = "generated",
        /// Top-level frame, automatic.
        AutoToplevel = "auto_toplevel",
        /// Result of a form submission.
        FormSubmit = "form_submit",
        /// Page reload.
        Reload = "reload",
        /// Keyword search.
        Keyword = "keyword",
        /// Keyword-generated.
        KeywordGenerated = "keyword_generated",
        /// Anything else.
        Other = "other",
    }
}

string_enum! {
    /// Navigation type reported by [`FrameNavigated`].
    pub enum NavigationType {
        /// Standard navigation.
        Navigation = "Navigation",
        /// Back-forward cache restore.
        BackForwardCacheRestore = "BackForwardCacheRestore",
    }
}

/// `Page.enable`.
#[derive(Debug, Clone, Default, Serialize)]
pub struct Enable;
impl CdpCommand for Enable {
    const METHOD: &'static str = "Page.enable";
    type Returns = Empty;
}

/// `Page.disable`.
#[derive(Debug, Clone, Default, Serialize)]
pub struct Disable;
impl CdpCommand for Disable {
    const METHOD: &'static str = "Page.disable";
    type Returns = Empty;
}

/// `Page.navigate` params.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Navigate {
    /// URL to navigate to.
    pub url: String,
    /// Referrer URL.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub referrer: Option<String>,
    /// Intended transition type.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub transition_type: Option<TransitionType>,
    /// Frame id to navigate; defaults to the main frame.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub frame_id: Option<FrameId>,
}

impl Navigate {
    /// Construct a navigate command for the main frame.
    pub fn new(url: impl Into<String>) -> Self {
        Self {
            url: url.into(),
            referrer: None,
            transition_type: None,
            frame_id: None,
        }
    }
}

/// Response for [`Navigate`].
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NavigateResult {
    /// Frame id that was navigated.
    pub frame_id: FrameId,
    /// Loader id of the navigation. `None` for fragment navigations.
    pub loader_id: Option<LoaderId>,
    /// User-friendly error message if navigation failed.
    pub error_text: Option<String>,
}

impl CdpCommand for Navigate {
    const METHOD: &'static str = "Page.navigate";
    type Returns = NavigateResult;
}

/// `Page.reload`.
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Reload {
    /// If true, ignores the cache.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ignore_cache: Option<bool>,
    /// If set, this script will be injected into all frames at load time.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub script_to_evaluate_on_load: Option<String>,
}
impl CdpCommand for Reload {
    const METHOD: &'static str = "Page.reload";
    type Returns = Empty;
}

/// `Page.captureScreenshot`.
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CaptureScreenshot {
    /// Image format. `None` defaults to PNG.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub format: Option<ImageFormat>,
    /// JPEG/WebP quality in `[0, 100]`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub quality: Option<u32>,
    /// `true` to capture beyond the viewport.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub capture_beyond_viewport: Option<bool>,
}

/// Response for [`CaptureScreenshot`].
#[derive(Debug, Clone, Deserialize)]
pub struct ScreenshotData {
    /// Base64-encoded image data.
    pub data: String,
}

impl CdpCommand for CaptureScreenshot {
    const METHOD: &'static str = "Page.captureScreenshot";
    type Returns = ScreenshotData;
}

/// `Page.printToPDF` (subset of params).
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PrintToPdf {
    /// Paper width, in inches.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub paper_width: Option<f64>,
    /// Paper height, in inches.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub paper_height: Option<f64>,
    /// Print landscape orientation.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub landscape: Option<bool>,
    /// Display header and footer.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub display_header_footer: Option<bool>,
    /// Print background graphics.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub print_background: Option<bool>,
    /// Scale of the webpage rendering. Default is 1.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scale: Option<f64>,
}

/// Response for [`PrintToPdf`].
#[derive(Debug, Clone, Deserialize)]
pub struct PdfData {
    /// Base64-encoded PDF bytes.
    pub data: String,
}

impl CdpCommand for PrintToPdf {
    const METHOD: &'static str = "Page.printToPDF";
    type Returns = PdfData;
}

/// `Page.addScriptToEvaluateOnNewDocument` — inject a script that runs on every
/// new document before any page scripts.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AddScriptToEvaluateOnNewDocument {
    /// Script source.
    pub source: String,
    /// World name to inject into. Defaults to the main world.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub world_name: Option<String>,
    /// Whether to inject into all frames.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub include_command_line_api: Option<bool>,
}

/// Response for [`AddScriptToEvaluateOnNewDocument`].
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AddedScript {
    /// Identifier of the script.
    pub identifier: ScriptId,
}

impl CdpCommand for AddScriptToEvaluateOnNewDocument {
    const METHOD: &'static str = "Page.addScriptToEvaluateOnNewDocument";
    type Returns = AddedScript;
}

/// `Page.removeScriptToEvaluateOnNewDocument`.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RemoveScriptToEvaluateOnNewDocument {
    /// Identifier returned by [`AddScriptToEvaluateOnNewDocument`].
    pub identifier: ScriptId,
}
impl CdpCommand for RemoveScriptToEvaluateOnNewDocument {
    const METHOD: &'static str = "Page.removeScriptToEvaluateOnNewDocument";
    type Returns = Empty;
}

/// `Page.setLifecycleEventsEnabled`.
#[derive(Debug, Clone, Serialize)]
pub struct SetLifecycleEventsEnabled {
    /// Whether to emit `Page.lifecycleEvent`.
    pub enabled: bool,
}
impl CdpCommand for SetLifecycleEventsEnabled {
    const METHOD: &'static str = "Page.setLifecycleEventsEnabled";
    type Returns = Empty;
}

/// `Page.frameNavigated` event.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FrameNavigated {
    /// Frame description after navigation.
    pub frame: serde_json::Value,
    /// Type of navigation. Older Chrome versions don't include this field.
    pub r#type: Option<NavigationType>,
}
impl CdpEvent for FrameNavigated {
    const METHOD: &'static str = "Page.frameNavigated";
    const ENABLE: Option<&'static str> = Some("Page.enable");
}

/// `Page.lifecycleEvent` event.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LifecycleEvent {
    /// Frame the event applies to.
    pub frame_id: FrameId,
    /// Loader id.
    pub loader_id: LoaderId,
    /// Event name (e.g. `"init"`, `"DOMContentLoaded"`, `"load"`).
    pub name: String,
    /// `Network.MonotonicTime` of the event.
    pub timestamp: f64,
}
impl CdpEvent for LifecycleEvent {
    const METHOD: &'static str = "Page.lifecycleEvent";
    const ENABLE: Option<&'static str> = Some("Page.enable");
}

/// `Page.loadEventFired` event.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LoadEventFired {
    /// `Network.MonotonicTime` of the event.
    pub timestamp: f64,
}
impl CdpEvent for LoadEventFired {
    const METHOD: &'static str = "Page.loadEventFired";
    const ENABLE: Option<&'static str> = Some("Page.enable");
}

/// Domain facade returned by [`Cdp::page`].
#[derive(Debug)]
pub struct PageDomain<'a> {
    cdp: &'a Cdp,
}

impl<'a> PageDomain<'a> {
    pub(crate) fn new(cdp: &'a Cdp) -> Self {
        Self {
            cdp,
        }
    }

    /// `Page.enable`.
    pub async fn enable(&self) -> WebDriverResult<()> {
        self.cdp.send(Enable).await?;
        Ok(())
    }

    /// `Page.disable`.
    pub async fn disable(&self) -> WebDriverResult<()> {
        self.cdp.send(Disable).await?;
        Ok(())
    }

    /// `Page.navigate`.
    pub async fn navigate(&self, url: impl Into<String>) -> WebDriverResult<NavigateResult> {
        self.cdp.send(Navigate::new(url)).await
    }

    /// `Page.reload`.
    pub async fn reload(&self) -> WebDriverResult<()> {
        self.cdp.send(Reload::default()).await?;
        Ok(())
    }

    /// `Page.captureScreenshot` — returns the raw base64-encoded image data.
    pub async fn capture_screenshot_base64(&self) -> WebDriverResult<String> {
        Ok(self.cdp.send(CaptureScreenshot::default()).await?.data)
    }

    /// `Page.printToPDF` — returns the raw base64-encoded PDF bytes.
    pub async fn print_to_pdf_base64(&self) -> WebDriverResult<String> {
        Ok(self.cdp.send(PrintToPdf::default()).await?.data)
    }

    /// `Page.addScriptToEvaluateOnNewDocument`.
    pub async fn add_script_to_evaluate_on_new_document(
        &self,
        source: impl Into<String>,
    ) -> WebDriverResult<ScriptId> {
        Ok(self
            .cdp
            .send(AddScriptToEvaluateOnNewDocument {
                source: source.into(),
                world_name: None,
                include_command_line_api: None,
            })
            .await?
            .identifier)
    }

    /// `Page.removeScriptToEvaluateOnNewDocument`.
    pub async fn remove_script_to_evaluate_on_new_document(
        &self,
        identifier: ScriptId,
    ) -> WebDriverResult<()> {
        self.cdp
            .send(RemoveScriptToEvaluateOnNewDocument {
                identifier,
            })
            .await?;
        Ok(())
    }

    /// `Page.setLifecycleEventsEnabled`.
    pub async fn set_lifecycle_events_enabled(&self, enabled: bool) -> WebDriverResult<()> {
        self.cdp
            .send(SetLifecycleEventsEnabled {
                enabled,
            })
            .await?;
        Ok(())
    }
}