eoka 0.5.11

Stealth browser automation for Rust. Puppeteer/Playwright alternative with anti-bot bypass.
Documentation
use eoka::Browser;
use serde_json::{json, Value};
use tokio::io::{AsyncReadExt, AsyncWriteExt};

struct Fixture {
    url: String,
    task: tokio::task::JoinHandle<()>,
}

impl Drop for Fixture {
    fn drop(&mut self) {
        self.task.abort();
    }
}

impl Fixture {
    async fn start(cross_site: bool) -> Self {
        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
        let port = listener.local_addr().unwrap().port();
        let middle_host = if cross_site { "b.test" } else { "a.test" };
        let inner_host = if cross_site { "c.test" } else { "a.test" };
        let task = tokio::spawn(async move {
            loop {
                let (mut socket, _) = listener.accept().await.unwrap();
                tokio::spawn(async move {
                    let mut data = [0; 8192];
                    let n = socket.read(&mut data).await.unwrap_or(0);
                    let request = String::from_utf8_lossy(&data[..n]);
                    let path = request.split_whitespace().nth(1).unwrap_or("/");
                    let html = match path {
                        "/" => format!(r#"<style>body{{margin:0}} iframe{{width:500px;height:400px;border:5px solid black;padding:2px;}}</style><iframe src="http://{middle_host}:{port}/middle"></iframe>"#),
                        "/middle" => format!(r#"<style>body{{margin:0}} iframe{{width:320px;height:200px;border:3px solid black;padding:4px;}}</style><iframe src="http://{inner_host}:{port}/inner"></iframe>"#),
                        _ => r#"<style>body{margin:0}button{position:absolute;left:40px;top:40px;width:80px;height:60px}</style><button id="target">fixture</button>"#.to_string(),
                    };
                    let response = format!("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", html.len(), html);
                    let _ = socket.write_all(response.as_bytes()).await;
                });
            }
        });
        Self {
            url: format!("http://a.test:{port}/"),
            task,
        }
    }
}

#[tokio::test]
#[ignore = "requires Chrome; uses only disposable local fixtures"]
async fn nested_frame_routing_ancestry_native_quads_and_stale_ids() {
    for isolated in [false, true] {
        let fixture = Fixture::start(isolated).await;
        let browser = Browser::launch_with(|config| {
            config.extra_args.extend([
                "--host-resolver-rules=MAP *.test 127.0.0.1".into(),
                "--no-proxy-server".into(),
            ]);
            if isolated {
                config.extra_args.extend([
                    "--disable-features=AutomationControlled,EnableAutomation".into(),
                    "--site-per-process".into(),
                ]);
            }
        })
        .await
        .unwrap();
        let page = browser.new_page(&fixture.url).await.unwrap();
        let inner = tokio::time::timeout(std::time::Duration::from_secs(10), async {
            loop {
                if let Some(inner) = page
                    .frames()
                    .await
                    .unwrap()
                    .into_iter()
                    .find(|f| f.url.ends_with("/inner"))
                {
                    let ready: bool = page
                        .evaluate_in_frame_id(&inner.id, "!!document.querySelector('#target')")
                        .await
                        .unwrap();
                    if ready {
                        break inner;
                    }
                }
                tokio::time::sleep(std::time::Duration::from_millis(100)).await;
            }
        })
        .await
        .expect("nested frame ready");
        let ancestry = page.frame_ancestor_ids(&inner.id).await.unwrap();
        assert_eq!(ancestry.len(), 2);
        assert!(page
            .frame_ancestor_ids(&ancestry[1])
            .await
            .unwrap()
            .is_empty());
        assert_eq!(
            page.frame_ancestor_ids(&ancestry[0]).await.unwrap(),
            vec![ancestry[1].clone()]
        );
        assert!(page.frame_ancestor_ids("unrelated-id").await.is_err());
        let targets: Value = page
            .session()
            .transport()
            .send("Target.getTargets", &json!({}))
            .await
            .unwrap();
        let oopifs = targets["targetInfos"]
            .as_array()
            .unwrap()
            .iter()
            .filter(|t| t["type"] == "iframe")
            .count();
        if isolated {
            assert!(oopifs >= 2, "fixture must exercise OOPIF routing");
        } else {
            assert_eq!(oopifs, 0, "fixture must exercise same-process routing");
        }
        let host: String = page
            .evaluate_in_frame_id(&inner.id, "location.hostname")
            .await
            .unwrap();
        assert_eq!(host, if isolated { "c.test" } else { "a.test" });
        let quad = page
            .frame_element_content_quad(&inner.id, "#target", 0)
            .await
            .unwrap();
        assert!(quad[2] > quad[0] && quad[5] > quad[3]);
        assert!(page
            .frame_element_content_quad(&inner.id, "#missing", 0)
            .await
            .is_err());
        assert!(page
            .frame_element_content_quad(&inner.id, "#target", 64)
            .await
            .is_err());
        assert!(page
            .frame_element_content_quad(&inner.id, &"x".repeat(4097), 0)
            .await
            .is_err());
        for mode in ["open", "closed"] {
            let script = format!(
                r#"(() => {{
                const element=document.querySelector('#target');
                const host=document.body.appendChild(document.createElement('div'));
                const root=host.attachShadow({{mode:'{mode}'}});
                root.innerHTML='<div style="transform:scaleX(-1)"><slot></slot></div>';
                host.appendChild(element);
                return true;
            }})()"#
            );
            page.evaluate_in_frame_id::<bool>(&inner.id, &script)
                .await
                .unwrap();
            let reflected = page
                .frame_element_content_quad(&inner.id, "#target", 0)
                .await
                .unwrap();
            assert!(
                reflected[2] < reflected[0],
                "{mode} shadow reflection: {reflected:?}"
            );
            page.evaluate_in_frame_id::<bool>(
                &inner.id,
                "(document.body.appendChild(document.querySelector('#target')),true)",
            )
            .await
            .unwrap();
            let supported = page
                .frame_element_content_quad(&inner.id, "#target", 0)
                .await
                .unwrap();
            assert!(supported[2] > supported[0]);
        }
        page.evaluate_in_frame_id::<bool>(
            &inner.id,
            "(document.querySelector('#target').style.display='none',true)",
        )
        .await
        .unwrap();
        assert!(page
            .frame_element_content_quad(&inner.id, "#target", 0)
            .await
            .is_err());
        page.evaluate_in_frame_id::<bool>(
            &inner.id,
            "(document.querySelector('#target').style.display='',true)",
        )
        .await
        .unwrap();
        page.evaluate_in_frame_id::<bool>(&inner.id, "(() => { for(let i=0;i<65;i++)document.body.appendChild(document.createElement('span'));return true; })()").await.unwrap();
        assert!(page
            .frame_element_content_quad(&inner.id, "span", 0)
            .await
            .is_err());
        let null: Option<String> = page.evaluate_in_frame_id(&inner.id, "null").await.unwrap();
        assert_eq!(null, None);
        let null: Option<String> = page.evaluate_sync("null").await.unwrap();
        assert_eq!(null, None);
        assert!(page
            .evaluate_in_frame_id::<Value>(&inner.id, "undefined")
            .await
            .is_err());
        let other = browser.new_page("about:blank").await.unwrap();
        assert!(other.frame_ancestor_ids(&inner.id).await.is_err());
        assert!(other
            .frame_element_content_quad(&inner.id, "#target", 0)
            .await
            .is_err());
        assert!(other
            .evaluate_in_frame_id::<Value>(&inner.id, "1")
            .await
            .is_err());
        page.evaluate_sync::<bool>("(document.querySelector('iframe').remove(), true)")
            .await
            .unwrap();
        assert!(page
            .evaluate_in_frame_id::<Value>(&inner.id, "1")
            .await
            .is_err());
        assert!(page.frame_ancestor_ids(&inner.id).await.is_err());
        assert!(page
            .frame_element_content_quad(&inner.id, "#target", 0)
            .await
            .is_err());
        browser.close().await.unwrap();
    }
}