allwright-surface-web 0.1.26

Web surface plugin for the allwright engine.
Documentation
use allwright_plugin_sdk::BrowserKind;
use allwright_surface_web::{
    click_element, close_browser_process, count_elements, fill_element, get_text_content,
    launch_browser, navigate_page,
};

use std::io::{Read, Write};
use std::net::TcpListener;
use std::sync::{
    Arc,
    atomic::{AtomicBool, Ordering},
};
use std::time::Duration;

struct Fixture {
    url: String,
    stop: Arc<AtomicBool>,
}
impl Fixture {
    fn new() -> Self {
        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
        let port = listener.local_addr().unwrap().port();
        listener.set_nonblocking(true).unwrap();
        let stop = Arc::new(AtomicBool::new(false));
        let stopped = stop.clone();
        std::thread::spawn(move || {
            while !stopped.load(Ordering::Relaxed) {
                let Ok((mut stream, _)) = listener.accept() else {
                    std::thread::sleep(Duration::from_millis(10));
                    continue;
                };
                stream
                    .set_read_timeout(Some(Duration::from_secs(2)))
                    .unwrap();
                let mut request = [0; 4096];
                let n = stream.read(&mut request).unwrap_or(0);
                if n == 0 {
                    continue;
                }
                let request = String::from_utf8_lossy(&request[..n]);
                let body = if request.starts_with("GET /slow ") {
                    std::thread::sleep(Duration::from_millis(800));
                    "<p>Loaded</p>".to_string()
                } else if request.starts_with("GET /frame ") {
                    r#"<title>Frame</title><input><button onclick="this.textContent='Clicked'; const frame=document.createElement('iframe'); frame.id='slow'; frame.src='/slow'; document.body.append(frame)">Cross origin frame action</button><iframe srcdoc="<p>Nested</p>"></iframe>"#.to_string()
                } else {
                    format!(
                        r#"<iframe id="child" src="http://localhost:{port}/frame"></iframe><iframe id="unstable" srcdoc="<body><script>setInterval(() => document.body.setAttribute('data-tick', Date.now()), 20)</script>"></iframe>"#
                    )
                };
                let response = format!(
                    "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
                    body.len(),
                    body
                );
                let _ = stream.write_all(response.as_bytes());
            }
        });
        Self {
            url: format!("http://127.0.0.1:{port}/"),
            stop,
        }
    }
}
impl Drop for Fixture {
    fn drop(&mut self) {
        self.stop.store(true, Ordering::Relaxed);
    }
}
struct BrowserProcess(u32);
impl Drop for BrowserProcess {
    fn drop(&mut self) {
        let pid = self.0;
        let _ = std::thread::spawn(move || close_browser_process(pid)).join();
    }
}

#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires installed Chromium/Firefox"]
async fn frames_are_independent_pages() {
    let kind = if std::env::var("ALLWRIGHT_TEST_BROWSER").as_deref() == Ok("firefox") {
        BrowserKind::Firefox
    } else {
        BrowserKind::Chromium
    };
    let browser = tokio::task::spawn_blocking(move || launch_browser(kind, None))
        .await
        .unwrap()
        .unwrap();
    let _process = BrowserProcess(browser.process_id);
    let fixture = Fixture::new();
    let parent = navigate_page(
        &browser.browser_session,
        &browser.initial_page.page_session,
        &fixture.url,
    )
    .await
    .unwrap()
    .page_session;
    assert!(
        allwright_surface_web::resolve_frame(&browser.browser_session, &parent, "iframe")
            .await
            .unwrap_err()
            .contains("exactly one")
    );
    assert!(
        allwright_surface_web::resolve_frame(&browser.browser_session, &parent, "#unstable")
            .await
            .unwrap_err()
            .contains("still loading or changing")
    );
    let started = std::time::Instant::now();
    let frame = allwright_surface_web::resolve_frame(&browser.browser_session, &parent, "#child")
        .await
        .unwrap()
        .page_session;
    assert!(started.elapsed() >= Duration::from_millis(200));
    assert_eq!(
        get_text_content(&browser.browser_session, &frame, "button")
            .await
            .unwrap()
            .text,
        "Cross origin frame action"
    );
    assert_eq!(
        count_elements(&browser.browser_session, &parent, "button")
            .await
            .unwrap()
            .count,
        0
    );
    let nested = allwright_surface_web::resolve_frame(&browser.browser_session, &frame, "iframe")
        .await
        .unwrap()
        .page_session;
    assert_eq!(
        get_text_content(&browser.browser_session, &nested, "p")
            .await
            .unwrap()
            .text,
        "Nested"
    );
    fill_element(&browser.browser_session, &frame, "input", "in frame")
        .await
        .unwrap();
    click_element(&browser.browser_session, &frame, "button")
        .await
        .unwrap();
    assert_eq!(
        get_text_content(&browser.browser_session, &frame, "button")
            .await
            .unwrap()
            .text,
        "Clicked"
    );
    assert!(
        allwright_surface_web::resolve_frame(&browser.browser_session, &frame, "#slow")
            .await
            .is_err()
    );
    let deadline = std::time::Instant::now() + Duration::from_secs(10);
    let loaded = loop {
        match allwright_surface_web::resolve_frame(&browser.browser_session, &frame, "#slow").await
        {
            Ok(page) => break page.page_session,
            Err(error) => {
                assert!(std::time::Instant::now() < deadline, "{error}");
                tokio::time::sleep(Duration::from_millis(50)).await;
            }
        }
    };
    assert_eq!(
        get_text_content(&browser.browser_session, &loaded, "p")
            .await
            .unwrap()
            .text,
        "Loaded"
    );
    assert!(
        allwright_surface_web::resolve_frame(&browser.browser_session, &frame, "button")
            .await
            .unwrap_err()
            .contains("iframe")
    );
    assert!(
        allwright_surface_web::resolve_frame(&browser.browser_session, &parent, ".missing")
            .await
            .is_err()
    );
    navigate_page(&browser.browser_session, &parent, "about:blank")
        .await
        .unwrap();
    tokio::time::sleep(Duration::from_millis(200)).await;
    assert!(
        count_elements(&browser.browser_session, &frame, "button")
            .await
            .is_err(),
        "detached frame must not fall back to parent"
    );
}