use serde_json::{json, Value};
use super::error::BridgeError;
use super::eval_synthesizer::{build_iife, build_iife_with_args};
use super::servo_backend::ServoBackend;
use super::{get_i64, get_opt_bool, get_opt_i64, get_opt_str, get_str, unit_ok};
fn evaluate_to_cdp_json(r: &super::servo_backend::EvaluateResult) -> Value {
let mut result = json!({
"type": r.result.type_,
"value": r.result.value.clone().unwrap_or(Value::Null),
});
if let Some(s) = &r.result.object_id {
result["objectId"] = Value::String(s.clone());
}
if let Some(e) = &r.exception_details {
return json!({
"result": result,
"exceptionDetails": {
"exceptionId": e.exception_id,
"text": e.text,
"lineNumber": e.line_number,
"columnNumber": e.column_number,
}
});
}
json!({ "result": result })
}
fn eval_iife(
backend: &dyn ServoBackend,
target_id: &str,
expression: String,
) -> Result<Value, BridgeError> {
let r = backend.runtime_evaluate(target_id, &expression)?;
Ok(evaluate_to_cdp_json(&r))
}
fn call_function_on_element(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
body: &str,
) -> Result<Value, BridgeError> {
let object_id = get_str(params, "objectId")?;
let r = backend.runtime_call_function_on(target_id, &object_id, body, &[])?;
Ok(evaluate_to_cdp_json(&r))
}
pub fn page_title(
backend: &dyn ServoBackend,
target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
let expr = build_iife("return document.title;");
eval_iife(backend, target_id, expr)
}
pub fn page_url(
backend: &dyn ServoBackend,
target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
let expr = build_iife("return location.href;");
eval_iife(backend, target_id, expr)
}
pub fn page_content(
backend: &dyn ServoBackend,
target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
let expr = build_iife("return document.documentElement.outerHTML;");
eval_iife(backend, target_id, expr)
}
pub fn page_viewport(
backend: &dyn ServoBackend,
target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
let m = backend.page_layout_metrics(target_id)?;
Ok(json!({
"width": m.layout_width,
"height": m.layout_height,
"deviceScaleFactor": 1,
"isMobile": false,
"hasTouch": false,
}))
}
pub fn page_set_viewport(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let width = get_i64(params, "width")?;
let height = get_i64(params, "height")?;
let device_scale_factor = {
let v = get_opt_i64(params, "deviceScaleFactor", 1);
if v < 0 {
1.0
} else {
v as f64
}
};
let mobile = get_opt_bool(params, "isMobile", false);
let metrics = super::servo_backend::DeviceMetrics {
width,
height,
device_scale_factor,
mobile,
};
unit_ok(backend.emulation_set_device_metrics(target_id, metrics))
}
const WAIT_FOR_DEFAULT_TIMEOUT_MS: u64 = 30_000;
const WAIT_FOR_MAX_TIMEOUT_MS: u64 = 600_000;
const NETWORK_IDLE_QUIET_MS: u64 = 500;
const ELEMENT_POLL_INTERVAL_MS: u64 = 100;
const NETWORK_ACTIVITY_METHODS: [&str; 4] = [
"Network.requestWillBeSent",
"Network.responseReceived",
"Network.loadingFinished",
"Network.loadingFailed",
];
fn wait_timeout_ms(params: &Value) -> u64 {
let requested = params
.get("timeout")
.and_then(|v| v.as_u64())
.unwrap_or(WAIT_FOR_DEFAULT_TIMEOUT_MS);
requested.min(WAIT_FOR_MAX_TIMEOUT_MS)
}
fn url_matcher(pattern: &str) -> impl Fn(&str) -> bool + Send {
let compiled = glob::Pattern::new(pattern).ok();
let owned = pattern.to_string();
move |url: &str| match &compiled {
Some(p) => p.matches(url),
None => url == owned,
}
}
fn probe_string(
backend: &dyn ServoBackend,
target_id: &str,
expression: &str,
) -> Result<Option<String>, BridgeError> {
let r = backend.runtime_evaluate(target_id, expression)?;
Ok(r
.result
.value
.and_then(|v| v.as_str().map(|s| s.to_string())))
}
fn require_tap<'a>(
backend: &'a dyn ServoBackend,
method: &str,
) -> Result<&'a std::sync::Arc<super::event_translator::CdpEventTap>, BridgeError> {
backend.event_tap().ok_or_else(|| {
BridgeError::NotSupported(format!(
"{method}: backend has no CDP event tap (host event pump not wired)"
))
})
}
pub fn page_wait_for_load_state(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let state = params
.get("state")
.and_then(|v| v.as_str())
.unwrap_or("load")
.to_string();
let timeout_ms = wait_timeout_ms(params);
match state.as_str() {
"load" => {
if probe_string(backend, target_id, "document.readyState")?
.map(|s| s == "complete")
.unwrap_or(false)
{
return Ok(json!({}));
}
let tap = require_tap(backend, "Page.waitForLoadState")?;
tap.wait(
target_id,
&["Page.lifecycleEvent", "Page.loadEventFired"],
Box::new(|p| {
match p.get("name").and_then(|v| v.as_str()) {
Some(name) => name == "load",
None => true,
}
}),
std::time::Duration::from_millis(timeout_ms),
)?;
Ok(json!({}))
}
"domcontentloaded" => {
if probe_string(backend, target_id, "document.readyState")?
.map(|s| s == "interactive" || s == "complete")
.unwrap_or(false)
{
return Ok(json!({}));
}
let tap = require_tap(backend, "Page.waitForLoadState")?;
tap.wait(
target_id,
&["Page.lifecycleEvent", "Page.loadEventFired"],
Box::new(|p| match p.get("name").and_then(|v| v.as_str()) {
Some(name) => name == "load",
None => true,
}),
std::time::Duration::from_millis(timeout_ms),
)?;
Ok(json!({}))
}
"networkidle" => {
let tap = require_tap(backend, "Page.waitForLoadState")?;
let deadline =
std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms);
loop {
let now = std::time::Instant::now();
if now >= deadline {
return Err(BridgeError::Timeout(format!(
"Page.waitForLoadState(networkidle): no idle window within {timeout_ms}ms"
)));
}
let quiet = std::cmp::min(
std::time::Duration::from_millis(NETWORK_IDLE_QUIET_MS),
deadline - now,
);
match tap.wait(
target_id,
&NETWORK_ACTIVITY_METHODS,
Box::new(|_| true),
quiet,
) {
Ok(_) => continue, Err(BridgeError::Timeout(_)) => return Ok(json!({})),
Err(e) => return Err(e),
}
}
}
other => Err(BridgeError::InvalidParams(format!(
"Page.waitForLoadState: unsupported state {other:?} (load | domcontentloaded | networkidle)"
))),
}
}
pub fn page_wait_for_url(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let pattern = params
.get("url")
.or_else(|| params.get("pattern"))
.and_then(|v| v.as_str())
.ok_or_else(|| {
BridgeError::InvalidParams("Page.waitForURL: missing string field: url".into())
})?
.to_string();
let matcher = url_matcher(&pattern);
if probe_string(backend, target_id, "location.href")?
.map(|u| matcher(&u))
.unwrap_or(false)
{
return Ok(json!({}));
}
let tap = require_tap(backend, "Page.waitForURL")?;
let main_frame = bao_cdp::servo_bridge::main_frame_id_for_target(target_id);
tap.wait(
target_id,
&["Page.frameNavigated"],
Box::new(move |p| {
let frame_matches = p
.get("frame")
.and_then(|f| f.get("id"))
.and_then(|v| v.as_str())
.map(|fid| fid == main_frame)
.unwrap_or(false);
let url_matches = p
.get("frame")
.and_then(|f| f.get("url"))
.and_then(|v| v.as_str())
.map(|u| matcher(u))
.unwrap_or(false);
frame_matches && url_matches
}),
std::time::Duration::from_millis(wait_timeout_ms(params)),
)?;
Ok(json!({}))
}
pub fn page_wait_for_request(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let tap = require_tap(backend, "Page.waitForRequest")?;
let matcher = params
.get("url")
.or_else(|| params.get("pattern"))
.and_then(|v| v.as_str())
.map(url_matcher);
tap.wait(
target_id,
&["Network.requestWillBeSent"],
Box::new(move |p| {
match &matcher {
Some(m) => p
.get("request")
.and_then(|r| r.get("url"))
.and_then(|v| v.as_str())
.map(|u| m(u))
.unwrap_or(false),
None => true,
}
}),
std::time::Duration::from_millis(wait_timeout_ms(params)),
)
}
pub fn page_wait_for_response(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let tap = require_tap(backend, "Page.waitForResponse")?;
let matcher = params
.get("url")
.or_else(|| params.get("pattern"))
.and_then(|v| v.as_str())
.map(url_matcher);
tap.wait(
target_id,
&["Network.responseReceived"],
Box::new(move |p| {
match &matcher {
Some(m) => p
.get("response")
.and_then(|r| r.get("url"))
.and_then(|v| v.as_str())
.map(|u| m(u))
.unwrap_or(false),
None => true,
}
}),
std::time::Duration::from_millis(wait_timeout_ms(params)),
)
}
pub fn page_wait_for_event(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let event = params
.get("event")
.and_then(|v| v.as_str())
.ok_or_else(|| {
BridgeError::InvalidParams("Page.waitForEvent: missing string field: event".into())
})?;
let methods: Vec<String> = if event.contains('.') {
vec![event.to_string()]
} else {
let mapped: &[&str] = match event {
"console" => &["Log.entryAdded"],
"pageerror" => &["Runtime.exceptionThrown"],
"request" => &["Network.requestWillBeSent"],
"requestfailed" => &["Network.loadingFailed"],
"requestfinished" => &["Network.loadingFinished"],
"response" => &["Network.responseReceived"],
"load" => &["Page.loadEventFired"],
"domcontentloaded" => &["Page.loadEventFired"],
"framenavigated" => &["Page.frameNavigated"],
other => {
return Err(BridgeError::InvalidParams(format!(
"Page.waitForEvent: no CDP event signal for {other:?} on this event face"
)))
}
};
mapped.iter().map(|s| s.to_string()).collect()
};
let tap = require_tap(backend, "Page.waitForEvent")?;
let method_refs: Vec<&str> = methods.iter().map(|s| s.as_str()).collect();
tap.wait(
target_id,
&method_refs,
Box::new(|_| true),
std::time::Duration::from_millis(wait_timeout_ms(params)),
)
}
pub fn page_go_back(
backend: &dyn ServoBackend,
target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
let expr = build_iife("history.back(); return true;");
eval_iife(backend, target_id, expr)
}
pub fn page_go_forward(
backend: &dyn ServoBackend,
target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
let expr = build_iife("history.forward(); return true;");
eval_iife(backend, target_id, expr)
}
pub fn page_emulate_media(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let media = get_opt_str(params, "media").unwrap_or_else(|| "screen".to_string());
let body = format!(
"// @trace REQ-BAO-API-005 [method:Page.emulateMedia]\nvar __m=__args[0];try{{window.matchMedia=window.matchMedia||function(){{return {{matches:false,addListener:function(){{}},removeListener:function(){{}}}};}};return __m;}}catch(e){{return false;}}"
);
let _ = backend; let _ = target_id;
eval_iife(
backend,
target_id,
build_iife_with_args(&body, &[json!(media)])?,
)
}
pub fn page_add_script_tag(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let url = get_opt_str(params, "url");
let content = get_opt_str(params, "content");
let mut args = vec![];
if let Some(u) = &url {
args.push(json!(u));
} else if let Some(c) = &content {
args.push(json!(c));
} else {
return Err(BridgeError::InvalidParams(
"addScriptTag requires url or content".into(),
));
}
let mode = if url.is_some() { "url" } else { "content" };
args.push(json!(mode));
let body = "var src=__args[0], mode=__args[1]; var s=document.createElement('script'); if(mode==='url'){s.src=src;} else {s.textContent=src;} document.head.appendChild(s); return true;";
let expr = build_iife_with_args(body, &args)?;
eval_iife(backend, target_id, expr)
}
pub fn page_add_style_tag(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let url = get_opt_str(params, "url");
let content = get_opt_str(params, "content");
let mut args = vec![];
if let Some(u) = &url {
args.push(json!(u));
} else if let Some(c) = &content {
args.push(json!(c));
} else {
return Err(BridgeError::InvalidParams(
"addStyleTag requires url or content".into(),
));
}
let mode = if url.is_some() { "url" } else { "content" };
args.push(json!(mode));
let body = "var src=__args[0], mode=__args[1]; if(mode==='url'){var l=document.createElement('link'); l.rel='stylesheet'; l.href=src; document.head.appendChild(l);} else {var s=document.createElement('style'); s.textContent=src; document.head.appendChild(s);} return true;";
let expr = build_iife_with_args(body, &args)?;
eval_iife(backend, target_id, expr)
}
pub fn page_expose_function(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let name = get_str(params, "name")?;
let body = "var n=__args[0]; try { window[n]=function(){return Promise.resolve(n+':called');}; return true; } catch(e){ return false; }";
let expr = build_iife_with_args(body, &[json!(name)])?;
eval_iife(backend, target_id, expr)
}
pub fn page_screenshot(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let fmt_str = get_opt_str(params, "type");
let fmt = super::servo_backend::BridgeScreenshotFormat::from_cdp(fmt_str.as_deref());
let bytes = backend.page_screenshot(target_id, fmt)?;
let b64 = super::a_class_handlers::base64_encode(&bytes);
Ok(json!({ "data": b64, "binary": bytes }))
}
pub fn page_pdf(
backend: &dyn ServoBackend,
target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
let bytes = backend.page_print_to_pdf(target_id)?;
let b64 = super::a_class_handlers::base64_encode(&bytes);
Ok(json!({ "data": b64 }))
}
pub fn page_tap(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let selector = get_str(params, "selector")?;
let body = "var s=__args[0]; var el=document.querySelector(s); if(!el){throw new Error('not found');} el.scrollIntoViewIfNeeded(); var r=el.getBoundingClientRect(); return [r.x+r.width/2, r.y+r.height/2];";
let expr = build_iife_with_args(body, &[json!(selector)])?;
let r = backend.runtime_evaluate(target_id, &expr)?;
if r.exception_details.is_some() {
return Err(BridgeError::ServoError("tap: selector not found".into()));
}
Ok(evaluate_to_cdp_json(&r))
}
pub fn page_hover(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let selector = get_str(params, "selector")?;
let body = "var s=__args[0]; var el=document.querySelector(s); if(!el){throw new Error('not found');} el.dispatchEvent(new MouseEvent('mouseenter',{bubbles:true})); el.dispatchEvent(new MouseEvent('mouseover',{bubbles:true})); return true;";
let expr = build_iife_with_args(body, &[json!(selector)])?;
eval_iife(backend, target_id, expr)
}
pub fn page_focus(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let selector = get_str(params, "selector")?;
let body = "var s=__args[0]; var el=document.querySelector(s); if(!el){throw new Error('not found');} el.focus(); return true;";
let expr = build_iife_with_args(body, &[json!(selector)])?;
eval_iife(backend, target_id, expr)
}
pub fn page_type(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let selector = get_str(params, "selector")?;
let text = get_str(params, "text")?;
let body = "var s=__args[0], t=__args[1]; var el=document.querySelector(s); if(!el){throw new Error('not found');} el.focus(); var ev=new InputEvent('input',{bubbles:true,data:t}); if(el.value!==undefined){el.value=el.value+t;} else {el.textContent=(el.textContent||'')+t;} el.dispatchEvent(ev); return true;";
let expr = build_iife_with_args(body, &[json!(selector), json!(text)])?;
eval_iife(backend, target_id, expr)
}
pub fn page_fill(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let selector = get_str(params, "selector")?;
let value = get_str(params, "value")?;
let body = "var s=__args[0], v=__args[1]; var el=document.querySelector(s); if(!el){throw new Error('not found');} el.focus(); if(el.value!==undefined){el.value=v;} else {el.textContent=v;} el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true})); return true;";
let expr = build_iife_with_args(body, &[json!(selector), json!(value)])?;
eval_iife(backend, target_id, expr)
}
pub fn page_press(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let selector = get_str(params, "selector")?;
let key = get_str(params, "key")?;
let body = "var s=__args[0], k=__args[1]; var el=document.querySelector(s); if(!el){throw new Error('not found');} el.focus(); el.dispatchEvent(new KeyboardEvent('keydown',{bubbles:true,key:k})); el.dispatchEvent(new KeyboardEvent('keyup',{bubbles:true,key:k})); return true;";
let expr = build_iife_with_args(body, &[json!(selector), json!(key)])?;
eval_iife(backend, target_id, expr)
}
pub fn page_check(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let selector = get_str(params, "selector")?;
let body = "var s=__args[0]; var el=document.querySelector(s); if(!el){throw new Error('not found');} el.checked=true; el.dispatchEvent(new Event('change',{bubbles:true})); return true;";
let expr = build_iife_with_args(body, &[json!(selector)])?;
eval_iife(backend, target_id, expr)
}
pub fn page_uncheck(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let selector = get_str(params, "selector")?;
let body = "var s=__args[0]; var el=document.querySelector(s); if(!el){throw new Error('not found');} el.checked=false; el.dispatchEvent(new Event('change',{bubbles:true})); return true;";
let expr = build_iife_with_args(body, &[json!(selector)])?;
eval_iife(backend, target_id, expr)
}
pub fn page_select_option(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let selector = get_str(params, "selector")?;
let values = params
.get("values")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let body = "var s=__args[0], vs=__args[1]; var el=document.querySelector(s); if(!el){throw new Error('not found');} var selected=[]; for(var i=0;i<vs.length;i++){var opt=Array.prototype.find.call(el.options,function(o){return o.value===vs[i];}); if(opt){opt.selected=true; selected.push(vs[i]);}} el.dispatchEvent(new Event('change',{bubbles:true})); return selected;";
let expr = build_iife_with_args(body, &[json!(selector), Value::Array(values)])?;
eval_iife(backend, target_id, expr)
}
pub fn page_set_input_files(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let selector = get_str(params, "selector")?;
let paths = params
.get("paths")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let body = "var s=__args[0], ps=__args[1]; var el=document.querySelector(s); if(!el){throw new Error('not found');} el.dispatchEvent(new CustomEvent('bao-set-input-files',{detail:ps,bubbles:true})); return true;";
let expr = build_iife_with_args(body, &[json!(selector), Value::Array(paths)])?;
eval_iife(backend, target_id, expr)
}
pub fn page_set_default_navigation_timeout(
_backend: &dyn ServoBackend,
_target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
Ok(Value::Object(Default::default()))
}
pub fn page_set_default_timeout(
_backend: &dyn ServoBackend,
_target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
Ok(Value::Object(Default::default()))
}
pub fn page_opener(
backend: &dyn ServoBackend,
target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
let expr = build_iife("return (window.opener ? true : false);");
eval_iife(backend, target_id, expr)
}
pub fn page_frames(
backend: &dyn ServoBackend,
target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
let tree = backend.page_frame_tree(target_id)?;
let mut frames = vec![frame_to_json(&tree.frame)];
collect_child_frames(&tree, &mut frames);
Ok(json!({ "frames": frames }))
}
fn frame_to_json(f: &super::servo_backend::Frame) -> Value {
json!({
"id": f.id,
"url": f.url,
"name": f.name,
"parentId": f.parent_id,
})
}
fn collect_child_frames(tree: &super::servo_backend::FrameTree, out: &mut Vec<Value>) {
for child in &tree.child_frames {
out.push(frame_to_json(&child.frame));
collect_child_frames(child, out);
}
}
pub fn page_main_frame(
backend: &dyn ServoBackend,
target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
let tree = backend.page_frame_tree(target_id)?;
Ok(frame_to_json(&tree.frame))
}
pub fn page_request_gc(
backend: &dyn ServoBackend,
target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
let expr = build_iife(
"if (typeof window.gc==='function') { window.gc(); return true; } return false;",
);
eval_iife(backend, target_id, expr)
}
pub fn element_content_frame(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let body = "return (this && this.contentWindow) ? {id:String(this.contentWindow.location.href)} : null;";
call_function_on_element(backend, target_id, params, body)
}
pub fn element_owner_frame(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let body = "return (this && this.ownerDocument && this.ownerDocument.defaultView) ? {id:String(this.ownerDocument.defaultView.location.href)} : null;";
call_function_on_element(backend, target_id, params, body)
}
pub fn element_get_attribute(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let object_id = get_str(params, "objectId")?;
let name = get_str(params, "name")?;
let body = "var n=__args[0]; return this ? this.getAttribute(n) : null;";
let r = backend.runtime_call_function_on(target_id, &object_id, body, &[json!(name)])?;
Ok(evaluate_to_cdp_json(&r))
}
pub fn element_scroll_into_view(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let body =
"if (this && this.scrollIntoViewIfNeeded) { this.scrollIntoViewIfNeeded(); } return true;";
call_function_on_element(backend, target_id, params, body)
}
pub fn element_inner_html(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let body = "return this ? this.innerHTML : null;";
call_function_on_element(backend, target_id, params, body)
}
pub fn element_inner_text(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let body = "return this ? this.innerText : null;";
call_function_on_element(backend, target_id, params, body)
}
pub fn element_text_content(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let body = "return this ? this.textContent : null;";
call_function_on_element(backend, target_id, params, body)
}
pub fn element_is_checked(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let body = "return !!(this && this.checked);";
call_function_on_element(backend, target_id, params, body)
}
pub fn element_is_disabled(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let body = "return !!(this && this.disabled);";
call_function_on_element(backend, target_id, params, body)
}
pub fn element_is_editable(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let body = "return !!(this && !this.disabled && !this.readOnly);";
call_function_on_element(backend, target_id, params, body)
}
pub fn element_is_enabled(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let body = "return !!(this && !this.disabled);";
call_function_on_element(backend, target_id, params, body)
}
pub fn element_is_hidden(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let body = "if(!this){return true;} var r=this.getBoundingClientRect(); var s=window.getComputedStyle(this); return (r.width===0||r.height===0)||s.visibility==='hidden'||s.display==='none';";
call_function_on_element(backend, target_id, params, body)
}
pub fn element_is_visible(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let body = "if(!this){return false;} var r=this.getBoundingClientRect(); var s=window.getComputedStyle(this); return (r.width>0&&r.height>0)&&s.visibility!=='hidden'&&s.display!=='none';";
call_function_on_element(backend, target_id, params, body)
}
pub fn element_wait_for_element_state(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let object_id = get_str(params, "objectId")?;
let state = params
.get("state")
.and_then(|v| v.as_str())
.ok_or_else(|| {
BridgeError::InvalidParams(
"ElementHandle.waitForElementState: missing string field: state".into(),
)
})?
.to_string();
let body: &str = match state.as_str() {
"visible" => {
"if(!this){return false;} var r=this.getBoundingClientRect(); var s=window.getComputedStyle(this); return (r.width>0&&r.height>0)&&s.visibility!=='hidden'&&s.display!=='none';"
}
"hidden" => {
"if(!this){return true;} var r=this.getBoundingClientRect(); var s=window.getComputedStyle(this); return (r.width===0||r.height===0)||s.visibility==='hidden'||s.display==='none';"
}
"enabled" => "return !!(this && !this.disabled);",
"disabled" => "return !!(this && this.disabled);",
"editable" => "return !!(this && !this.disabled && !this.readOnly);",
other => {
return Err(BridgeError::InvalidParams(format!(
"ElementHandle.waitForElementState: unsupported state {other:?} \
(visible | hidden | enabled | disabled | editable; 'stable' has no \
frame signal on this face)"
)))
}
};
let deadline =
std::time::Instant::now() + std::time::Duration::from_millis(wait_timeout_ms(params));
loop {
let r = backend.runtime_call_function_on(target_id, &object_id, body, &[])?;
if r.result.value.as_ref().and_then(|v| v.as_bool()).unwrap_or(false) {
return Ok(json!({}));
}
if std::time::Instant::now() >= deadline {
return Err(BridgeError::Timeout(format!(
"ElementHandle.waitForElementState({state}): condition unmet within {}ms",
wait_timeout_ms(params)
)));
}
std::thread::sleep(std::time::Duration::from_millis(ELEMENT_POLL_INTERVAL_MS));
}
}
pub fn element_wait_for_selector(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let selector = get_str(params, "selector")?;
let state = params
.get("state")
.and_then(|v| v.as_str())
.unwrap_or("visible")
.to_string();
if !matches!(state.as_str(), "visible" | "attached" | "hidden" | "detached") {
return Err(BridgeError::InvalidParams(format!(
"ElementHandle.waitForSelector: unsupported state {state:?} \
(visible | attached | hidden | detached)"
)));
}
let visibility_expr = build_iife_with_args(
"var s=__args[0]; var el=document.querySelector(s); if(!el){return false;} \
var r=el.getBoundingClientRect(); var cs=window.getComputedStyle(el); \
return (r.width>0&&r.height>0)&&cs.visibility!=='hidden'&&cs.display!=='none';",
&[json!(selector)],
)?;
let is_visible = |b: &dyn ServoBackend| -> Result<bool, BridgeError> {
let r = b.runtime_evaluate(target_id, &visibility_expr)?;
Ok(r.result.value.as_ref().and_then(|v| v.as_bool()).unwrap_or(false))
};
let deadline =
std::time::Instant::now() + std::time::Duration::from_millis(wait_timeout_ms(params));
loop {
let found = backend
.dom_query_selector(target_id, 0, &selector)?
.unwrap_or(0);
let satisfied = match state.as_str() {
"attached" => found != 0,
"visible" => found != 0 && is_visible(backend)?,
"hidden" => found == 0 || !is_visible(backend)?,
"detached" => found == 0,
other => unreachable!("state validated above: {other}"),
};
if satisfied {
return Ok(json!({ "nodeId": found }));
}
if std::time::Instant::now() >= deadline {
return Err(BridgeError::Timeout(format!(
"ElementHandle.waitForSelector({state}): condition unmet within {}ms",
wait_timeout_ms(params)
)));
}
std::thread::sleep(std::time::Duration::from_millis(ELEMENT_POLL_INTERVAL_MS));
}
}
pub fn js_handle_as_element(
_backend: &dyn ServoBackend,
_target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
Ok(json!({ "isElement": false }))
}
pub fn js_handle_dispose(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
super::a_class_handlers::runtime_release_object(backend, target_id, params)
}
fn js_handle_call_function(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let object_id = get_str(params, "objectId")?;
let func = get_str(params, "func")?;
let args: Vec<Value> = params
.get("args")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let r = backend.runtime_call_function_on(target_id, &object_id, &func, &args)?;
Ok(evaluate_to_cdp_json(&r))
}
pub fn js_handle_evaluate(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
js_handle_call_function(backend, target_id, params)
}
pub fn js_handle_evaluate_handle(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
js_handle_call_function(backend, target_id, params)
}
pub fn js_handle_get_properties(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let object_id = get_str(params, "objectId")?;
let own = get_opt_bool(params, "ownProperties", true);
let props = backend.runtime_get_properties(target_id, &object_id, own)?;
let arr: Vec<Value> = props
.iter()
.map(|p| {
json!({
"name": p.name,
"value": p.value.as_ref().map(|v| v.type_.clone()),
"isOwn": p.is_own,
})
})
.collect();
Ok(json!({ "result": arr, "internalProperties": [] }))
}
pub fn js_handle_get_property(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let object_id = get_str(params, "objectId")?;
let name = get_str(params, "name")?;
let body = "var n=__args[0]; return this ? this[n] : null;";
let r = backend.runtime_call_function_on(target_id, &object_id, body, &[json!(name)])?;
Ok(evaluate_to_cdp_json(&r))
}
pub fn js_handle_json_value(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let body = "return this ? JSON.parse(JSON.stringify(this)) : null;";
call_function_on_element(backend, target_id, params, body)
}
#[cfg(test)]
mod tests {
use super::super::servo_backend::MockServoBackend;
use super::*;
use serde_json::json;
fn backend() -> MockServoBackend {
let mut b = MockServoBackend::new();
b.add_target("1");
b
}
#[test]
fn page_title_generates_iife() {
let b = backend();
let r = page_title(&b, "1", &json!({})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("(function(){"));
assert!(v.contains("return document.title;"));
assert!(v.ends_with("})()"));
}
#[test]
fn page_url_generates_iife() {
let b = backend();
let r = page_url(&b, "1", &json!({})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("return location.href;"));
}
#[test]
fn page_content_generates_iife() {
let b = backend();
let r = page_content(&b, "1", &json!({})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("document.documentElement.outerHTML"));
}
#[test]
fn page_viewport_returns_layout_metrics() {
let b = backend();
let r = page_viewport(&b, "1", &json!({})).unwrap();
assert!(r["width"].is_number());
assert!(r["height"].is_number());
}
#[test]
fn page_set_viewport_calls_emulation_override() {
let b = backend();
let r = page_set_viewport(&b, "1", &json!({"width":800,"height":600})).unwrap();
assert_eq!(r.as_object().unwrap().len(), 0);
}
#[test]
fn add_script_tag_with_url_injection_attempt() {
let b = backend();
let payload = "'; alert('xss'); //";
let r = page_add_script_tag(&b, "1", &json!({"url": payload})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("\"'; alert('xss'); //\""));
assert!(v.contains("var src=__args[0]"));
assert!(!v.contains(&format!("s.src={payload}")));
}
#[test]
fn add_script_tag_with_content_injection_attempt() {
let b = backend();
let payload = "</script><script>alert('xss')</script>";
let r = page_add_script_tag(&b, "1", &json!({"content": payload})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("\"</script>"));
assert!(v.contains("s.textContent=src;"));
}
#[test]
fn add_style_tag_with_url_injection_attempt() {
let b = backend();
let payload = "'; alert(1); //";
let r = page_add_style_tag(&b, "1", &json!({"url": payload})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("\"'; alert(1); //\""));
assert!(v.contains("var src=__args[0]"));
}
#[test]
fn expose_function_name_injection_attempt() {
let b = backend();
let payload = r#"x');alert('pwn');//"#;
let r = page_expose_function(&b, "1", &json!({"name": payload})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("var n=__args[0]"));
let args_marker = "var __args=";
let args_pos = v.find(args_marker).unwrap();
let args_end_rel = v[args_pos..].find("];").unwrap();
let args_literal = &v[args_pos..args_pos + args_end_rel + 1];
assert!(args_literal.contains("\"x');alert('pwn');//\""));
}
#[test]
fn page_tap_with_selector_injection_attempt() {
let b = backend();
let payload = "'); alert('x'); //";
let r = page_tap(&b, "1", &json!({"selector": payload})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("\"'); alert('x'); //\""));
assert!(v.contains("var s=__args[0]"));
}
#[test]
fn page_type_with_text_injection_attempt() {
let b = backend();
let payload = "');alert(String.fromCharCode(88,83,83));//";
let r = page_type(&b, "1", &json!({"selector":"input","text":payload})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("var s=__args[0], t=__args[1]"));
assert!(v.contains("\"');alert(String.fromCharCode(88,83,83));//\""));
}
#[test]
fn page_fill_with_value_injection_attempt() {
let b = backend();
let payload = "\\\";alert(1);//";
let r = page_fill(&b, "1", &json!({"selector":"input","value":payload})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("var s=__args[0], v=__args[1]"));
assert!(v.contains("\\\\"));
}
#[test]
fn page_press_with_key_injection_attempt() {
let b = backend();
let payload = "Enter');alert('x');//";
let r = page_press(&b, "1", &json!({"selector":"input","key":payload})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("\"Enter');alert('x');//\""));
assert!(v.contains("var s=__args[0], k=__args[1]"));
}
#[test]
fn page_check_selector_injection_attempt() {
let b = backend();
let payload = "x']||alert(1);//";
let r = page_check(&b, "1", &json!({"selector":payload})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("\"x']||alert(1);//\""));
}
#[test]
fn page_uncheck_selector_injection_attempt() {
let b = backend();
let payload = "x';}alert(1);{//";
let r = page_uncheck(&b, "1", &json!({"selector":payload})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("\"x';}alert(1);{//\""));
}
#[test]
fn page_select_option_values_injection_attempt() {
let b = backend();
let payloads = vec![json!("';alert(1);//"), json!("</option>")];
let r =
page_select_option(&b, "1", &json!({"selector":"select","values":payloads})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("\"';alert(1);//\""));
assert!(v.contains("\"</option>\""));
}
#[test]
fn page_set_input_files_paths_injection_attempt() {
let b = backend();
let payloads = vec![json!("/etc/passwd'),alert(1),String('/")];
let r = page_set_input_files(
&b,
"1",
&json!({"selector":"input[type=file]","paths":payloads}),
)
.unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("var s=__args[0], ps=__args[1]"));
}
#[test]
fn page_focus_selector_injection_attempt() {
let b = backend();
let payload = "x' or '1'='1";
let r = page_focus(&b, "1", &json!({"selector":payload})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("\"x' or '1'='1\""));
}
#[test]
fn page_hover_selector_injection_attempt() {
let b = backend();
let payload = "x'//svg/onload=alert(1)//";
let r = page_hover(&b, "1", &json!({"selector":payload})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("\"x'//svg/onload=alert(1)//\""));
}
#[test]
fn element_get_attribute_injection_attempt() {
let b = backend();
let payload = "onclick';alert(1);//";
let r = element_get_attribute(&b, "1", &json!({"objectId":"obj1","name":payload})).unwrap();
assert!(r["result"].is_object());
}
#[test]
fn element_inner_html_uses_call_function_on() {
let b = backend();
let r = element_inner_html(&b, "1", &json!({"objectId":"obj1"})).unwrap();
assert!(r["result"].is_object());
}
#[test]
fn element_inner_text_uses_call_function_on() {
let b = backend();
let r = element_inner_text(&b, "1", &json!({"objectId":"obj1"})).unwrap();
assert!(r["result"].is_object());
}
#[test]
fn element_text_content_uses_call_function_on() {
let b = backend();
let r = element_text_content(&b, "1", &json!({"objectId":"obj1"})).unwrap();
assert!(r["result"].is_object());
}
#[test]
fn element_is_checked_uses_call_function_on() {
let b = backend();
let r = element_is_checked(&b, "1", &json!({"objectId":"obj1"})).unwrap();
assert!(r["result"].is_object());
}
#[test]
fn element_is_disabled_uses_call_function_on() {
let b = backend();
let r = element_is_disabled(&b, "1", &json!({"objectId":"obj1"})).unwrap();
assert!(r["result"].is_object());
}
#[test]
fn element_is_editable_uses_call_function_on() {
let b = backend();
let r = element_is_editable(&b, "1", &json!({"objectId":"obj1"})).unwrap();
assert!(r["result"].is_object());
}
#[test]
fn element_is_enabled_uses_call_function_on() {
let b = backend();
let r = element_is_enabled(&b, "1", &json!({"objectId":"obj1"})).unwrap();
assert!(r["result"].is_object());
}
#[test]
fn element_is_hidden_uses_call_function_on() {
let b = backend();
let r = element_is_hidden(&b, "1", &json!({"objectId":"obj1"})).unwrap();
assert!(r["result"].is_object());
}
#[test]
fn element_is_visible_uses_call_function_on() {
let b = backend();
let r = element_is_visible(&b, "1", &json!({"objectId":"obj1"})).unwrap();
assert!(r["result"].is_object());
}
#[test]
fn element_content_frame_uses_call_function_on() {
let b = backend();
let r = element_content_frame(&b, "1", &json!({"objectId":"obj1"})).unwrap();
assert!(r["result"].is_object());
}
#[test]
fn element_owner_frame_uses_call_function_on() {
let b = backend();
let r = element_owner_frame(&b, "1", &json!({"objectId":"obj1"})).unwrap();
assert!(r["result"].is_object());
}
#[test]
fn element_scroll_into_view_uses_call_function_on() {
let b = backend();
let r = element_scroll_into_view(&b, "1", &json!({"objectId":"obj1"})).unwrap();
assert!(r["result"].is_object());
}
fn backend_with_tap()
-> (MockServoBackend, std::sync::Arc<super::super::event_translator::CdpEventTap>) {
let mut b = MockServoBackend::new();
b.add_target("1");
let tap = std::sync::Arc::new(super::super::event_translator::CdpEventTap::new());
b.event_tap = Some(tap.clone());
(b, tap)
}
#[test]
fn element_wait_for_element_state_polls_until_true() {
let mut b = MockServoBackend::new();
b.add_target("1");
b.call_function_on_value = Some(json!(true));
let r = element_wait_for_element_state(
&b,
"1",
&json!({"objectId":"obj1","state":"visible","timeout": 500}),
)
.unwrap();
assert_eq!(r.as_object().unwrap().len(), 0);
let log = b.call_log.lock().unwrap();
assert!(
log.iter()
.any(|(_, m, _)| m == "runtime_call_function_on"),
"state predicate must poll via Runtime.callFunctionOn"
);
}
#[test]
fn element_wait_for_element_state_times_out() {
let b = backend();
let err = element_wait_for_element_state(
&b,
"1",
&json!({"objectId":"obj1","state":"enabled","timeout": 120}),
)
.unwrap_err();
assert!(matches!(err, BridgeError::Timeout(_)), "got: {err:?}");
}
#[test]
fn element_wait_for_selector_attached_returns_node_id() {
let b = backend();
let r = element_wait_for_selector(
&b,
"1",
&json!({"selector":"div","state":"attached","timeout": 500}),
)
.unwrap();
assert_eq!(r["nodeId"], 2);
}
#[test]
fn page_wait_for_request_without_tap_is_not_supported() {
let b = backend();
let err = page_wait_for_request(&b, "1", &json!({"url":"**/api/*"})).unwrap_err();
assert!(matches!(err, BridgeError::NotSupported(_)), "got: {err:?}");
}
#[test]
fn js_handle_as_element_returns_local_state() {
let b = backend();
let r = js_handle_as_element(&b, "1", &json!({"objectId":"obj1"})).unwrap();
assert_eq!(r["isElement"], false);
}
#[test]
fn js_handle_dispose_calls_release_object() {
let b = backend();
let r = js_handle_dispose(&b, "1", &json!({"objectId":"obj1"})).unwrap();
assert_eq!(r.as_object().unwrap().len(), 0);
}
#[test]
fn js_handle_evaluate_calls_call_function_on() {
let b = backend();
let r =
js_handle_evaluate(&b, "1", &json!({"objectId":"obj1","func":"return 1+1;"})).unwrap();
assert!(r["result"].is_object());
}
#[test]
fn js_handle_evaluate_handle_calls_call_function_on() {
let b = backend();
let r =
js_handle_evaluate_handle(&b, "1", &json!({"objectId":"obj1","func":"return this;"}))
.unwrap();
assert!(r["result"].is_object());
}
#[test]
fn js_handle_get_properties_calls_get_properties() {
let b = backend();
let r = js_handle_get_properties(&b, "1", &json!({"objectId":"obj1"})).unwrap();
assert!(r["result"].is_array());
}
#[test]
fn js_handle_get_property_injection_attempt() {
let b = backend();
let payload = "constructor';alert(1);//";
let r =
js_handle_get_property(&b, "1", &json!({"objectId":"obj1","name":payload})).unwrap();
assert!(r["result"].is_object());
}
#[test]
fn js_handle_json_value_uses_call_function_on() {
let b = backend();
let r = js_handle_json_value(&b, "1", &json!({"objectId":"obj1"})).unwrap();
assert!(r["result"].is_object());
}
#[test]
fn page_opener_generates_iife() {
let b = backend();
let r = page_opener(&b, "1", &json!({})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("window.opener"));
}
#[test]
fn page_frames_returns_array() {
let b = backend();
let r = page_frames(&b, "1", &json!({})).unwrap();
assert!(r["frames"].is_array());
}
#[test]
fn page_main_frame_returns_root() {
let b = backend();
let r = page_main_frame(&b, "1", &json!({})).unwrap();
assert!(r["id"].is_string() || r["id"].is_number());
}
#[test]
fn page_request_gc_generates_iife() {
let b = backend();
let r = page_request_gc(&b, "1", &json!({})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("window.gc"));
}
#[test]
fn page_go_back_generates_iife() {
let b = backend();
let r = page_go_back(&b, "1", &json!({})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("history.back"));
}
#[test]
fn page_go_forward_generates_iife() {
let b = backend();
let r = page_go_forward(&b, "1", &json!({})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("history.forward"));
}
#[test]
fn page_emulate_media_serializes_media_param() {
let b = backend();
let r = page_emulate_media(&b, "1", &json!({"media":"print"})).unwrap();
let v = r["result"]["value"].as_str().unwrap();
assert!(v.contains("\"print\""));
}
#[test]
fn page_screenshot_returns_base64_data() {
let b = backend();
let r = page_screenshot(&b, "1", &json!({})).unwrap();
assert!(r["data"].is_string());
assert!(r["binary"].is_array());
}
#[test]
fn page_pdf_returns_base64_data() {
let b = backend();
let r = page_pdf(&b, "1", &json!({})).unwrap();
assert!(r["data"].is_string());
}
#[test]
fn page_set_default_timeout_returns_empty() {
let b = backend();
let r = page_set_default_timeout(&b, "1", &json!({"timeout":30000})).unwrap();
assert_eq!(r.as_object().unwrap().len(), 0);
}
#[test]
fn page_set_default_navigation_timeout_returns_empty() {
let b = backend();
let r = page_set_default_navigation_timeout(&b, "1", &json!({"timeout":60000})).unwrap();
assert_eq!(r.as_object().unwrap().len(), 0);
}
#[test]
fn page_wait_for_load_state_resolves_on_load_lifecycle_event() {
let (b, tap) = backend_with_tap();
let feeder = std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(30));
tap.observe(
"1",
"Page.lifecycleEvent",
&json!({ "frameId": "main-1", "loaderId": "loader-1", "name": "init" }),
);
std::thread::sleep(std::time::Duration::from_millis(30));
tap.observe(
"1",
"Page.lifecycleEvent",
&json!({ "frameId": "main-1", "loaderId": "loader-1", "name": "load" }),
);
});
let r = page_wait_for_load_state(&b, "1", &json!({"state":"load","timeout": 2000}))
.unwrap();
assert_eq!(r.as_object().unwrap().len(), 0);
feeder.join().unwrap();
}
#[test]
fn page_wait_for_load_state_networkidle_resolves_on_quiet_window() {
let (b, tap) = backend_with_tap();
let start = std::time::Instant::now();
let r = page_wait_for_load_state(
&b,
"1",
&json!({"state":"networkidle","timeout": 5000}),
)
.unwrap();
assert!(
start.elapsed() >= std::time::Duration::from_millis(400),
"networkidle must respect the quiet window, resolved in {:?}",
start.elapsed()
);
assert_eq!(r.as_object().unwrap().len(), 0);
assert_eq!(tap.waiter_count(), 0);
}
#[test]
fn page_wait_for_url_filters_to_main_frame() {
let (b, tap) = backend_with_tap();
let feeder = std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(30));
tap.observe(
"1",
"Page.frameNavigated",
&json!({ "frame": { "id": "frame-child", "url": "https://x/done" } }),
);
std::thread::sleep(std::time::Duration::from_millis(50));
tap.observe(
"1",
"Page.frameNavigated",
&json!({ "frame": { "id": "main-1", "url": "https://x/done" } }),
);
});
page_wait_for_url(&b, "1", &json!({"url":"**/done","timeout": 2000})).unwrap();
feeder.join().unwrap();
}
#[test]
fn page_wait_for_request_returns_matching_event_params() {
let (b, tap) = backend_with_tap();
let feeder = std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(30));
tap.observe(
"1",
"Network.requestWillBeSent",
&json!({ "requestId": "r-wrong", "request": { "url": "https://x/static/logo.png" } }),
);
std::thread::sleep(std::time::Duration::from_millis(50));
tap.observe(
"1",
"Network.requestWillBeSent",
&json!({ "requestId": "r-1", "request": { "url": "https://x/api/data" } }),
);
});
let r = page_wait_for_request(&b, "1", &json!({"url":"**/api/*","timeout": 2000}))
.unwrap();
assert_eq!(r["requestId"], "r-1");
assert_eq!(r["request"]["url"], "https://x/api/data");
feeder.join().unwrap();
}
#[test]
fn page_wait_for_response_returns_matching_event_params() {
let (b, tap) = backend_with_tap();
let feeder = std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(30));
tap.observe(
"1",
"Network.responseReceived",
&json!({
"requestId": "r-1",
"response": { "url": "https://x/api/data", "status": 200 }
}),
);
});
let r = page_wait_for_response(&b, "1", &json!({"url":"**/api/*","timeout": 2000}))
.unwrap();
assert_eq!(r["response"]["status"], 200);
feeder.join().unwrap();
}
#[test]
fn page_wait_for_event_maps_playwright_names_and_rejects_unsignaled() {
let (b, tap) = backend_with_tap();
let feeder = std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(30));
tap.observe(
"1",
"Network.responseReceived",
&json!({ "requestId": "r-1", "response": { "url": "https://x/", "status": 204 } }),
);
});
let r = page_wait_for_event(&b, "1", &json!({"event":"response","timeout": 2000})).unwrap();
assert_eq!(r["response"]["status"], 204);
feeder.join().unwrap();
let err = page_wait_for_event(&b, "1", &json!({"event":"frameattached"})).unwrap_err();
assert!(matches!(err, BridgeError::InvalidParams(_)), "got: {err:?}");
}
#[test]
fn page_wait_for_request_times_out() {
let (b, tap) = backend_with_tap();
let err = page_wait_for_request(&b, "1", &json!({"timeout": 100})).unwrap_err();
assert!(matches!(err, BridgeError::Timeout(_)), "got: {err:?}");
assert_eq!(tap.waiter_count(), 0, "timeout must unregister the waiter");
}
#[test]
fn add_script_tag_missing_url_and_content_returns_invalid_params() {
let b = backend();
let err = page_add_script_tag(&b, "1", &json!({})).unwrap_err();
assert!(matches!(err, BridgeError::InvalidParams(_)));
}
#[test]
fn add_style_tag_missing_url_and_content_returns_invalid_params() {
let b = backend();
let err = page_add_style_tag(&b, "1", &json!({})).unwrap_err();
assert!(matches!(err, BridgeError::InvalidParams(_)));
}
#[test]
fn expose_function_missing_name_returns_invalid_params() {
let b = backend();
let err = page_expose_function(&b, "1", &json!({})).unwrap_err();
assert!(matches!(err, BridgeError::InvalidParams(_)));
}
#[test]
fn page_tap_missing_selector_returns_invalid_params() {
let b = backend();
let err = page_tap(&b, "1", &json!({})).unwrap_err();
assert!(matches!(err, BridgeError::InvalidParams(_)));
}
#[test]
fn page_type_missing_text_returns_invalid_params() {
let b = backend();
let err = page_type(&b, "1", &json!({"selector":"input"})).unwrap_err();
assert!(matches!(err, BridgeError::InvalidParams(_)));
}
#[test]
fn element_get_attribute_missing_object_id_returns_invalid_params() {
let b = backend();
let err = element_get_attribute(&b, "1", &json!({"name":"x"})).unwrap_err();
assert!(matches!(err, BridgeError::InvalidParams(_)));
}
}