rustenium 1.1.10

A modern, robust, high-performance WebDriver BiDi automation library for Rust
Documentation
use rustenium::browsers::{
    BidiBrowser, ChromeBrowser,
    cdp_browser::{AddPreloadScriptOptions, CdpBrowser, FetchNodeOptions},
};
use rustenium::nodes::{AXNode, Node};
use rustenium_cdp_definitions::browser_protocol::dom::types::BackendNodeId;

async fn first_backend_node_id(browser: &mut ChromeBrowser, url: &str) -> BackendNodeId {
    <ChromeBrowser as CdpBrowser>::navigate(browser, url)
        .await
        .unwrap();
    let nodes = <ChromeBrowser as CdpBrowser>::get_accessible_nodes(browser, false)
        .await
        .unwrap();
    let id = nodes
        .iter()
        .find_map(|n| n.backend_dom_node_id)
        .expect("expected at least one accessibility node with a backend_dom_node_id");
    BackendNodeId::new(id)
}

pub async fn test_navigate_to_url(mut browser: ChromeBrowser) {
    let result = <ChromeBrowser as CdpBrowser>::navigate(&mut browser, "https://example.com").await;
    assert!(result.is_ok(), "navigate should succeed");
    browser.close().await.unwrap();
}

pub async fn test_navigate_returns_frame_or_loader(mut browser: ChromeBrowser) {
    let result = <ChromeBrowser as CdpBrowser>::navigate(&mut browser, "https://example.com")
        .await
        .unwrap();
    assert!(
        !result.frame_id.inner().is_empty() || result.loader_id.is_some(),
        "navigate result should contain frame or loader id"
    );
    assert!(
        result.error_text.is_none(),
        "navigation should not error, got: {:?}",
        result.error_text
    );
    browser.close().await.unwrap();
}

pub async fn test_navigate_multiple_pages(mut browser: ChromeBrowser) {
    <ChromeBrowser as CdpBrowser>::navigate(&mut browser, "https://example.com")
        .await
        .unwrap();
    <ChromeBrowser as CdpBrowser>::navigate(&mut browser, "https://httpbin.org/html")
        .await
        .unwrap();
    browser.close().await.unwrap();
}

pub async fn test_get_accessible_nodes_not_empty(mut browser: ChromeBrowser) {
    <ChromeBrowser as CdpBrowser>::navigate(&mut browser, "https://example.com")
        .await
        .unwrap();
    let nodes = <ChromeBrowser as CdpBrowser>::get_accessible_nodes(&mut browser, false)
        .await
        .unwrap();
    assert!(
        !nodes.is_empty(),
        "should have at least one accessibility node"
    );
    browser.close().await.unwrap();
}

pub async fn test_get_accessible_nodes_squash_lte_full(mut browser: ChromeBrowser) {
    <ChromeBrowser as CdpBrowser>::navigate(&mut browser, "https://example.com")
        .await
        .unwrap();
    let full = <ChromeBrowser as CdpBrowser>::get_accessible_nodes(&mut browser, false)
        .await
        .unwrap();
    let squashed = <ChromeBrowser as CdpBrowser>::get_accessible_nodes(&mut browser, true)
        .await
        .unwrap();

    fn count(nodes: &[AXNode]) -> usize {
        nodes.iter().map(|n| 1 + count(&n.children)).sum()
    }
    assert!(
        count(&squashed) <= count(&full),
        "squashed tree ({}) should have <= nodes than full tree ({})",
        count(&squashed),
        count(&full)
    );
    browser.close().await.unwrap();
}

pub async fn test_get_accessible_nodes_squash_no_blank_generic(mut browser: ChromeBrowser) {
    <ChromeBrowser as CdpBrowser>::navigate(&mut browser, "https://example.com")
        .await
        .unwrap();
    let nodes = <ChromeBrowser as CdpBrowser>::get_accessible_nodes(&mut browser, true)
        .await
        .unwrap();

    fn is_blank_generic(node: &AXNode) -> bool {
        let name_blank = node.name_str().is_none_or(|s| s.trim().is_empty());
        let role_empty = node
            .role_str()
            .is_none_or(|r| matches!(r, "none" | "generic" | "group"));
        name_blank && role_empty
    }

    fn check(nodes: &[AXNode], parent_blank_generic: bool) {
        for node in nodes {
            let blank = is_blank_generic(node);
            assert!(
                !(blank && parent_blank_generic),
                "squashed tree should not contain a blank-generic child of a blank-generic parent, found node_id: {}",
                node.node_id
            );
            check(&node.children, blank);
        }
    }
    check(&nodes, false);
    browser.close().await.unwrap();
}

pub async fn test_fetch_node_by_node_id(mut browser: ChromeBrowser) {
    let backend_id = first_backend_node_id(&mut browser, "https://example.com").await;
    let node = <ChromeBrowser as CdpBrowser>::fetch_node(
        &mut browser,
        FetchNodeOptions::new().backend_node_id(backend_id).depth(1),
    )
    .await
    .unwrap();
    assert!(
        node.get_node_type().is_some(),
        "fetched node should have a valid node type"
    );
    browser.close().await.unwrap();
}

pub async fn test_fetch_node_with_depth_returns_children(mut browser: ChromeBrowser) {
    let backend_id = first_backend_node_id(&mut browser, "https://example.com").await;
    let shallow = <ChromeBrowser as CdpBrowser>::fetch_node(
        &mut browser,
        FetchNodeOptions::new().backend_node_id(backend_id).depth(1),
    )
    .await
    .unwrap();
    let deep = <ChromeBrowser as CdpBrowser>::fetch_node(
        &mut browser,
        FetchNodeOptions::new().backend_node_id(backend_id).depth(5),
    )
    .await
    .unwrap();
    assert!(
        deep.get_children_nodes().len() >= shallow.get_children_nodes().len(),
        "deeper fetch should return at least as many children"
    );
    browser.close().await.unwrap();
}

pub async fn test_emulate_device_metrics(mut browser: ChromeBrowser) {
    let result =
        <ChromeBrowser as CdpBrowser>::emulate_device_metrics(&mut browser, 375, 812, 3.0, true)
            .await;
    assert!(result.is_ok(), "emulate_device_metrics should succeed");
    browser.close().await.unwrap();
}

pub async fn test_create_tab(mut browser: ChromeBrowser) {
    let result =
        <ChromeBrowser as CdpBrowser>::create_tab(&mut browser, "https://example.com").await;
    assert!(result.is_ok(), "create_tab should succeed");
    browser.close().await.unwrap();
}

pub async fn test_preload_script_add_runs_and_remove(mut browser: ChromeBrowser) {
    let script_id = <ChromeBrowser as CdpBrowser>::add_preload_script_with_options(
        &mut browser,
        "globalThis.__rustenium_preload__ = 'ran';",
        AddPreloadScriptOptions::default().run_immediately(true),
    )
    .await
    .expect("add_preload_script_with_options should succeed");
    assert!(
        !script_id.inner().is_empty(),
        "identifier should not be empty"
    );

    <ChromeBrowser as CdpBrowser>::navigate(&mut browser, "https://example.com")
        .await
        .unwrap();
    let ran = <ChromeBrowser as CdpBrowser>::evaluate_script(
        &mut browser,
        "globalThis.__rustenium_preload__",
        false,
    )
    .await
    .unwrap();
    assert_eq!(
        ran.result.value,
        Some(serde_json::json!("ran")),
        "preload script should set the global on navigation"
    );

    <ChromeBrowser as CdpBrowser>::remove_preload_script(&mut browser, script_id)
        .await
        .expect("remove_preload_script should succeed");
    <ChromeBrowser as CdpBrowser>::navigate(&mut browser, "https://example.org")
        .await
        .unwrap();
    let after = <ChromeBrowser as CdpBrowser>::evaluate_script(
        &mut browser,
        "typeof globalThis.__rustenium_preload__",
        false,
    )
    .await
    .unwrap();
    assert_eq!(
        after.result.value,
        Some(serde_json::json!("undefined")),
        "removed preload should not run on subsequent navigation"
    );

    browser.close().await.unwrap();
}