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();
}
}